Fork me on GitHub

C++之那些年踩过的坑(附录一)

这篇内容呀,其实是接上一篇的结尾提出的一个小问题而写的,如果你还没有看过那么你可以先看一下。

真相

在上一篇的最后,我提到一个问题:代码优化。并留下了一个小小的测试:无符号数和有符号数的性能比较。不知道有没有童鞋去实验一下呢?那我们一起来做一个简单的实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <chrono>
int main()
{
/*unsigned */int a = 123456789;
auto t1 = std::chrono::system_clock::now();
for (int i = 0; i < 10000; ++i)
{
a += 3;
a -= 5;
a *= 11;
a /= 7;
}
auto t2 = std::chrono::system_clock::now();
auto runtime = std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1);
std::cout << "a = " << a << "\n";
std::cout << runtime.count() << "ns";
}

在上述代码在 VS 的 Debug 模式下运行,稳定后运行时间在 210000ns 左右,然后把注释去掉,再次运行,稳定后运行时间也是 210000ns 左右。在我在电脑上,计算有符号类型和无符号类型几乎是没有差别的,我相信在绝大多数的电脑上也是相同的结果。

类似的问题还有:浮点数的计算比整型数慢?对于这个问题,可以看一下这个问题:https://www.zhihu.com/question/26494062 。里面提供大量实验数据供参考。

无论如何,实际情况下就是,在当代,计算无符号数和有符号数的效率,甚至是计算浮点数和整数的效率,差异并不大。

我想说明什么呢?首先我要声明:我不是反对代码优化。而是对于有很多流传广泛的所谓的优化技巧,我觉得我们应该应该抱着学习探索的心态,而不是一味地追求一些没有什么意义的东西。有些优化,确实很精妙。但很多所谓的技巧,看起来的意思就是:做编译器的那群人都是傻逼。想优化我们的程序,这是正常的、应该的想法,但我们应该用科学的方法,而不是听了一些奇淫技巧,却不知道实际发生了什么。

其实很简单,探究性能瓶颈靠 profiling,探究代码背后的不为人知的故事看 assembly。我们先讲后面一个。

利用 assembly 探究代码

怎么得到反汇编代码,我在 这一篇 当中有提到。比如我们研究有符号数和无符号数,先写一个程序:

1
2
3
4
5
6
int main()
{
/*unsigned */int a = 123456789;
a = a / 13;
std::cout << a;
}

然后在 VS 下查看反汇编:

1
2
3
4
5
6
7
8
/*unsigned */int a = 123456789;
00B4104E mov dword ptr [a],75BCD15h
a = a / 13;
00B41055 mov eax,dword ptr [a]
00B41058 cdq
00B41059 mov ecx,0Dh
00B4105E idiv eax,ecx
00B41060 mov dword ptr [a],eax

把注释去掉再看:

1
2
3
4
5
6
7
8
unsigned int a = 123456789;
00C3104E mov dword ptr [a],75BCD15h
a = a / 13;
00C31055 mov eax,dword ptr [a]
00C31058 xor edx,edx
00C3105A mov ecx,0Dh
00C3105F div eax,ecx
00C31061 mov dword ptr [a],eax

几乎是一模一样的,最大的差别就是有符号数使用 idiv指令(带符号除法),无符号数使用 div指令(不带符号除法),而这两种指令,CPU 周期都是一样的。http://www.agner.org/optimize/instruction_tables.pdf

当然我不是说不用无符号数,而是说我们用什么要看场合,而不是你觉得用了性能更好,除非是被大众认可的或者你经过严谨的测试的。像对于某些书籍或者什么地方说,只要确定范围不为负数的,就用无符号类型,我是不认可的。如果你讲范围,那如果一个有符号类型不够用,那么通常它对应的无符号类型(相同的 bits)也不够用。比如你 std::int32_t 不够用,就应该用 std::int64_t,如果还不够,考虑写个 BigInteger类 吧。不过对于无符号和有符号类型,它们之间的性能在当代确实是几乎没有什么差别。那具体什么场合用什么呢?这个也不一定,比如一般来说:

  • 对于位储存、位运算、模运算等,使用无符号类型
  • 对于一般运算使用有符号类型

这一篇的目的,当然不只是谈有符号数和无符号数,而是想借题引出一些我的看法:

  • 过早的优化是万恶之源
  • 不要试图帮编译器优化
  • 优化时不要去猜测,想当然得去优化自己“觉得”性能不好的地方

我们一点一点讲。

对于一个需求,我们应该先完成功能,若性能达不到要求之后,在确定瓶颈之后再去优化。过早优化,不仅让代码不直接,还容易出 bug,还可能对性能几乎没有影响。而且,我们优化时,应该关注大方向,确定大方向是正确的。比如写一个算法,我们首先应该确保 Big-O 的时间复杂度能达标,可以用 O(n) 的就不用 O(nlogn),可以用 O(nlogn) 的就不用 O(n²),而不是先在那里扣 i++ 还是 ++i。另外,不要想着去帮编译器优化,因为编译器是一堆比你强不知道多少的人写出来的,而对于一般人,想着去帮编译器优化,大部分是无效的,甚至是错的。比如有人学了一点点的 std::move,就老是想着 move move move 去提高性能,举个栗子,容易写出这样的代码:

1
2
3
4
5
6
7
template <typename T>
T foo()
{
T object;
// ...
return std::move(object);
}

确实运行不会错,但是,代码背后做的事情不一定就跟你想的一样,往往跟你想象的还不一样。有些情况编译器可以采用更好的办法,结果因为你那么一搞,迫不得已只能采用次一点的办法。可以看看 这个问题,就不赘述了。

还有比如说用异或来交换两个变量,有人会想,用位运算,不仅不需要创建临时变量,而且位运算一般不是更快嘛!对于这个问题,陈硕大大早有讲到,在 https://cloud.github.com/downloads/chenshuo/documents/CppPractice.pdf 的第 9 章。可以去看一下。

如果你已经看了上面的链接,那么你也就知道了,你(几乎)不会知道编译器做了什么,编译器可以做的优化超出你的想象(不过有的时候人能明显看出来的优化编译器却做不到,但影响不大),在我的 系列文章(二) 中也有强调了,不要试图帮助编译器去优化。若你想探究一小段代码背后不为人知的故事,就去看看反汇编!

利用 profiling 探究瓶颈

关于这个问题,我想贴上一个链接,是 蓝色大大 的一个回答:https://www.zhihu.com/question/56727144/answer/150555866。有非常详细清晰的介绍。

结语

最后我希望我放的链接都有认真看呀!为什么我贴那么多链接呢?因为我说的话没有权威没人信啊!但大牛们说的总有参考价值了吧!

暂时先写那么多,才学疏浅,如有不当地方还请海涵,感谢指点!

-------------------------------- 全文完 感谢您的阅读 --------------------------------
「写的那么辛苦,连一块钱都不打赏吗/(ㄒoㄒ)/~~」