在32位系统上使用int64_t而不是int32_t会有什么样的性能影响?
我们的C ++库目前使用time_t来存储时间值。 在某些地方,我开始需要亚秒级精度,所以无论如何需要更大的数据types。 另外,在某些地方解决2038年问题也许是有用的。 所以我正在考虑完全切换到一个具有底层int64_t值的单个Time类,以取代所有地方的time_t值。
现在我想知道在32位操作系统或32位CPU上运行此代码时,这种更改对性能的影响。 IIUC编译器将生成使用32位寄存器执行64位算术的代码。 但是如果速度太慢,我可能不得不采用更加差异化的方式来处理时间值,这可能会使软件更难以维护。
我感兴趣的是:
- 哪些因素影响这些操作的性能? 可能是编译器和编译器的版本; 但操作系统或CPU制造商/型号是否也影响了这一点? 一个普通的32位系统将使用现代CPU的64位寄存器吗?
- 哪些操作在32位模拟时特别慢? 还是哪个几乎没有放缓?
- 在32位系统上使用int64_t / uint64_t是否有任何现有的基准testing结果?
- 有没有人有这种性能影响的自己的经验?
我主要对Intel Core 2系统上Linux 2.6(RHEL5,RHEL6)上的g ++ 4.1和4.4感兴趣; 但是也能很好地了解其他系统的情况(如Sparc Solaris + Solaris CC,Windows + MSVC)。
哪些因素影响这些操作的性能? 可能是编译器和编译器的版本; 但操作系统或CPU制造商/型号是否也影响了这一点?
大多数处理器体系结构(和模型 – 请阅读本节中提到处理器体系结构的模型)。 编译器可能会有一些影响,但大多数编译器在这方面做得相当好,所以处理器体系结构将比编译器有更大的影响。
操作系统不会有任何影响(除了“如果你改变操作系统,你需要使用不同types的编译器,这会改变编译器在某些情况下所做的工作 – 但这可能是一个小的影响)。
一个普通的32位系统将使用现代CPU的64位寄存器吗?
这不可能。 如果系统处于32位模式,它将作为一个32位系统,额外的32位寄存器是完全不可见的,就像系统实际上是一个“真正的32位系统”一样。 。
哪些操作在32位模拟时特别慢? 还是哪个几乎没有放缓?
加法和减法更糟,因为这些操作必须按照两个操作的顺序完成,而第二个操作需要第一个操作完成 – 如果编译器只对独立数据生成两个加法操作,情况就不是这样。
例如,如果input参数实际上是64位,那么多重复制将会变得更糟 – 所以2 ^ 35 * 83比2 ^ 31 * 2 ^ 31差。 这是因为处理器可以很好地产生一个32位乘以64位的结果 – 大约5-10个时钟周期。 但64×64位乘法需要一些额外的代码,所以需要更长的时间。
除法是一个与乘法相似的问题 – 但是这里可以在一边取一个64位的input,用一个32位的值除以得到一个32位的值。 由于很难预测什么时候会起作用,所以64位分隔可能几乎总是很慢。
数据还需要两倍的caching空间,这可能会影响结果。 而作为一个类似的结果,一般的分配和传递数据的时间至less需要两倍,因为有两倍的数据可以操作。
编译器也需要使用更多的寄存器。
在32位系统上使用int64_t / uint64_t是否有任何现有的基准testing结果?
也许,但我不知道任何。 即使有,它也只对你有意义,因为操作的组合对于操作的速度是非常关键的。
如果性能是您的应用程序的重要组成部分,然后基准您的代码(或其代表性的一部分)。 如果Benchmark X在相同的情况下速度较慢或速度较慢,那么速度将会降低5%,25%或103%,这并不重要。
有没有人有这种性能影响的自己的经验?
我已经重新编译了一些使用64位整数的64位体系结构的代码,发现性能提高了很多,比如一些代码的性能提高了25%。
更改你的操作系统到64位版本的相同的操作系统,可能会有所帮助,也许?
编辑:
因为我想知道这些东西有什么区别,所以我写了一些代码,还有一些原始的模板(仍然在学习 – 模板并不是我最热门的话题,我必须说 – 给我bitfiddling和指针算术,我会(通常)得到它的权利…)
这里是我写的代码,试图复制一些常见的函数:
#include <iostream> #include <cstdint> #include <ctime> using namespace std; static __inline__ uint64_t rdtsc(void) { unsigned hi, lo; __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi)); return ( (uint64_t)lo)|( ((uint64_t)hi)<<32 ); } template<typename T> static T add_numbers(const T *v, const int size) { T sum = 0; for(int i = 0; i < size; i++) sum += v[i]; return sum; } template<typename T, const int size> static T add_matrix(const T v[size][size]) { T sum[size] = {}; for(int i = 0; i < size; i++) { for(int j = 0; j < size; j++) sum[i] += v[i][j]; } T tsum=0; for(int i = 0; i < size; i++) tsum += sum[i]; return tsum; } template<typename T> static T add_mul_numbers(const T *v, const T mul, const int size) { T sum = 0; for(int i = 0; i < size; i++) sum += v[i] * mul; return sum; } template<typename T> static T add_div_numbers(const T *v, const T mul, const int size) { T sum = 0; for(int i = 0; i < size; i++) sum += v[i] / mul; return sum; } template<typename T> void fill_array(T *v, const int size) { for(int i = 0; i < size; i++) v[i] = i; } template<typename T, const int size> void fill_array(T v[size][size]) { for(int i = 0; i < size; i++) for(int j = 0; j < size; j++) v[i][j] = i + size * j; } uint32_t bench_add_numbers(const uint32_t v[], const int size) { uint32_t res = add_numbers(v, size); return res; } uint64_t bench_add_numbers(const uint64_t v[], const int size) { uint64_t res = add_numbers(v, size); return res; } uint32_t bench_add_mul_numbers(const uint32_t v[], const int size) { const uint32_t c = 7; uint32_t res = add_mul_numbers(v, c, size); return res; } uint64_t bench_add_mul_numbers(const uint64_t v[], const int size) { const uint64_t c = 7; uint64_t res = add_mul_numbers(v, c, size); return res; } uint32_t bench_add_div_numbers(const uint32_t v[], const int size) { const uint32_t c = 7; uint32_t res = add_div_numbers(v, c, size); return res; } uint64_t bench_add_div_numbers(const uint64_t v[], const int size) { const uint64_t c = 7; uint64_t res = add_div_numbers(v, c, size); return res; } template<const int size> uint32_t bench_matrix(const uint32_t v[size][size]) { uint32_t res = add_matrix(v); return res; } template<const int size> uint64_t bench_matrix(const uint64_t v[size][size]) { uint64_t res = add_matrix(v); return res; } template<typename T> void runbench(T (*func)(const T *v, const int size), const char *name, T *v, const int size) { fill_array(v, size); uint64_t long t = rdtsc(); T res = func(v, size); t = rdtsc() - t; cout << "result = " << res << endl; cout << name << " time in clocks " << dec << t << endl; } template<typename T, const int size> void runbench2(T (*func)(const T v[size][size]), const char *name, T v[size][size]) { fill_array(v); uint64_t long t = rdtsc(); T res = func(v); t = rdtsc() - t; cout << "result = " << res << endl; cout << name << " time in clocks " << dec << t << endl; } int main() { // spin up CPU to full speed... time_t t = time(NULL); while(t == time(NULL)) ; const int vsize=10000; uint32_t v32[vsize]; uint64_t v64[vsize]; uint32_t m32[100][100]; uint64_t m64[100][100]; runbench(bench_add_numbers, "Add 32", v32, vsize); runbench(bench_add_numbers, "Add 64", v64, vsize); runbench(bench_add_mul_numbers, "Add Mul 32", v32, vsize); runbench(bench_add_mul_numbers, "Add Mul 64", v64, vsize); runbench(bench_add_div_numbers, "Add Div 32", v32, vsize); runbench(bench_add_div_numbers, "Add Div 64", v64, vsize); runbench2(bench_matrix, "Matrix 32", m32); runbench2(bench_matrix, "Matrix 64", m64); }
编译:
g++ -Wall -m32 -O3 -o 32vs64 32vs64.cpp -std=c++0x
结果如下 : 注意:请参见下面的2016年结果 – 由于SSE指令在64位模式下的使用不同,这些结果稍微乐观一些,但在32位模式下没有使用SSE。
result = 49995000 Add 32 time in clocks 20784 result = 49995000 Add 64 time in clocks 30358 result = 349965000 Add Mul 32 time in clocks 30182 result = 349965000 Add Mul 64 time in clocks 79081 result = 7137858 Add Div 32 time in clocks 60167 result = 7137858 Add Div 64 time in clocks 457116 result = 49995000 Matrix 32 time in clocks 22831 result = 49995000 Matrix 64 time in clocks 23823
正如你所看到的,加法和乘法没有那么糟糕。 分部变得非常糟糕。 有趣的是,matrix加法没有太大的差别。
64位的速度是否更快?我听到有些人会问:使用相同的编译器选项,只是-m64而不是-m32 – yupp,速度要快很多:
result = 49995000 Add 32 time in clocks 8366 result = 49995000 Add 64 time in clocks 16188 result = 349965000 Add Mul 32 time in clocks 15943 result = 349965000 Add Mul 64 time in clocks 35828 result = 7137858 Add Div 32 time in clocks 50176 result = 7137858 Add Div 64 time in clocks 50472 result = 49995000 Matrix 32 time in clocks 12294 result = 49995000 Matrix 64 time in clocks 14733
编辑,2016年更新 :在编译器的32位和64位模式下,有四种变体(带和不带SSE)。
我通常使用clang ++作为我通常的编译器。 我试着用g ++编译(但是它仍然是不同于上面的版本,因为我已经更新了我的机器,而且我也有不同的CPU)。 由于g ++无法编译64位的no-sse版本,所以我没有看到这一点。 (无论如何,g ++都有类似的结果)
作为一个简短的表格:
Test name | no-sse 32 | no-sse 64 | sse 32 | sse 64 | ---------------------------------------------------------- Add uint32_t | 20837 | 10221 | 3701 | 3017 | ---------------------------------------------------------- Add uint64_t | 18633 | 11270 | 9328 | 9180 | ---------------------------------------------------------- Add Mul 32 | 26785 | 18342 | 11510 | 11562 | ---------------------------------------------------------- Add Mul 64 | 44701 | 17693 | 29213 | 16159 | ---------------------------------------------------------- Add Div 32 | 44570 | 47695 | 17713 | 17523 | ---------------------------------------------------------- Add Div 64 | 405258 | 52875 | 405150 | 47043 | ---------------------------------------------------------- Matrix 32 | 41470 | 15811 | 21542 | 8622 | ---------------------------------------------------------- Matrix 64 | 22184 | 15168 | 13757 | 12448 |
完整的结果与编译选项。
$ clang++ -m32 -mno-sse 32vs64.cpp --std=c++11 -O2 $ ./a.out result = 49995000 Add 32 time in clocks 20837 result = 49995000 Add 64 time in clocks 18633 result = 349965000 Add Mul 32 time in clocks 26785 result = 349965000 Add Mul 64 time in clocks 44701 result = 7137858 Add Div 32 time in clocks 44570 result = 7137858 Add Div 64 time in clocks 405258 result = 49995000 Matrix 32 time in clocks 41470 result = 49995000 Matrix 64 time in clocks 22184 $ clang++ -m32 -msse 32vs64.cpp --std=c++11 -O2 $ ./a.out result = 49995000 Add 32 time in clocks 3701 result = 49995000 Add 64 time in clocks 9328 result = 349965000 Add Mul 32 time in clocks 11510 result = 349965000 Add Mul 64 time in clocks 29213 result = 7137858 Add Div 32 time in clocks 17713 result = 7137858 Add Div 64 time in clocks 405150 result = 49995000 Matrix 32 time in clocks 21542 result = 49995000 Matrix 64 time in clocks 13757 $ clang++ -m64 -msse 32vs64.cpp --std=c++11 -O2 $ ./a.out result = 49995000 Add 32 time in clocks 3017 result = 49995000 Add 64 time in clocks 9180 result = 349965000 Add Mul 32 time in clocks 11562 result = 349965000 Add Mul 64 time in clocks 16159 result = 7137858 Add Div 32 time in clocks 17523 result = 7137858 Add Div 64 time in clocks 47043 result = 49995000 Matrix 32 time in clocks 8622 result = 49995000 Matrix 64 time in clocks 12448 $ clang++ -m64 -mno-sse 32vs64.cpp --std=c++11 -O2 $ ./a.out result = 49995000 Add 32 time in clocks 10221 result = 49995000 Add 64 time in clocks 11270 result = 349965000 Add Mul 32 time in clocks 18342 result = 349965000 Add Mul 64 time in clocks 17693 result = 7137858 Add Div 32 time in clocks 47695 result = 7137858 Add Div 64 time in clocks 52875 result = 49995000 Matrix 32 time in clocks 15811 result = 49995000 Matrix 64 time in clocks 15168
更多比你想知道在32位模式下做64位math…
在32位模式下(即使在64位CPU上,如果编译为32位代码)使用64位数字时,它们将存储为两个单独的32位数字,其中一个存储数字的较高位,另一个存储低位。 这个影响取决于一个指令。 (tl; dr – 一般来说,在32位CPU上执行64位math的速度在理论上是慢了2倍,只要你不划分/模数,但实际上差距会更小(1.3x是我的猜测),因为通常程序不只是在64位整数上进行math运算,还因为stream水线化,程序中的差异可能会小很多)。
加法/减法
许多体系结构支持所谓的进位标志 。 当加法结果溢出或减法结果不下溢时置位。 这些位的行为可以用长加法和长减法来表示。 在这个例子中,C表示高于最高可表示位(在操作期间)或进位标志(在操作之后)。
C 7 6 5 4 3 2 1 0 C 7 6 5 4 3 2 1 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 + 0 0 0 0 0 0 0 1 - 0 0 0 0 0 0 0 1 = 1 0 0 0 0 0 0 0 0 = 0 1 1 1 1 1 1 1 1
为什么携带标志是相关的? 那么,CPU通常会有两个独立的加法和减法操作。 在x86中,添加操作称为add
和adc
。 add
代表加法,而adc
加adc
。 这两者之间的区别在于, adc
考虑了一个进位,如果它被设置了,它就把结果加1。
同样,如果进位未被置位,带进位的减法结果减1。
这种行为允许在整数上轻松实现任意大小的加减。 x和y的加法结果(假设它们是8位)决不会大于0x1FE
。 如果你加1
,你得到0x1FF
。 因此9位足以表示任何8位加法的结果。 如果你使用add
,然后用adc
添加任何比特数,那么你可以添加任何你喜欢的数据。
在32位CPU上添加两个64位值如下所示。
- 将b的前32位添加到a的前32位。
- 加上后面的32位b到后面的32位a 。
类比减法。
这给出了2条指令,但是由于指令pipe道的原因 ,它可能比这慢,因为一个计算取决于另一个计算完成,所以如果CPU没有比64位加法还要做的事,CPU可能会等待第一个补充要做。
乘法
在x86上发生这种情况时,可以使用imul
和mul
以使溢出存储在edx寄存器中。 因此,将两个32位值相乘得到64位值非常容易。 这样的乘法是一条指令,但要利用它,必须将一个乘法值存储在eax中 。
无论如何,对于两个64位值相乘的更一般的情况,可以使用下面的公式(假定函数r除去32位以上的位)来计算它们。
首先,很容易注意到结果的低32位将乘以低32位相乘的variables。 这是由于同余关系。
a 1≡b 1 (mod n )
a 2≡b 2 (mod n )
a 1 a 2≡b 1 b 2 (mod n )
因此,任务仅限于确定更高的32位。 要计算结果的高32位,应将下列值相加。
- 低32位的高32位乘法(CPU可以存储在edx中的溢出)
- 第一variables的高32位与第二variables的低32位相乘
- 第一个variables的低32位乘以第二个variables的高32位
这给出了大约5条指令,但是由于x86中的寄存器数量相对有限(忽略了对体系结构的扩展),所以它们不能占用stream水线的太多优势。 如果要提高乘法速度,启用SSE,因为这会增加寄存器的数量。
部门/模块(两者在实现上相似)
我不知道它是如何工作的,但它比加法,减法甚至乘法复杂得多。 然而,它可能比64位CPU上的分割慢30倍。 如果你能理解,我可以查看“艺术计算机程序devise第2卷:研究mathalgorithm”(第257页)了解更多细节(不幸的是,我不能以我能解释的方式)。
如果你除以2的幂,请参考移位部分,因为这是本质上编译器可以优化除法(加上移位之前的最高有效位为有符号数字)。
和/或/ XOR
考虑到这些操作是单比特操作,这里没有什么特别的事情,只是按位操作两次。
左右移动
有趣的是,x86实际上有一个执行64位左移的指令,称为shld
,它不是用零replace最低有效位,而是用不同寄存器的最高有效位取代它们。 同样, shrd
指令也是这种情况。 这很容易使64位移位两个指令的操作。
但是,这只是一个不断变化的情况。 当一个移位不固定的时候,事情就会变得更加复杂,因为x86架构只支持以0-31作为值的移位。 除此之外的任何东西都是根据官方文档未定义的,在实践中,用0x1F按位进行操作是在一个值上执行的。 因此,当移位值高于31时,一个值存储器被完全擦除(对于左移,对于右移,这是更低的字节,这是更高的字节)。 另一个获得被擦除的寄存器中的值,然后执行移位操作。 这个结果取决于分支预测器做出好的预测,并且由于需要检查一个值而有点慢。
__builtin_popcount [11]
__builtin_popcount(低)+ __builtin_popcount(高)
其他内置
我现在懒得完成答案。 有人甚至使用这些?
无符号与有符号
加法,减法,乘法或和xor左移生成完全相同的代码。 右移仅使用稍微不同的代码(算术移位与逻辑移位),但结构上相同。 然而,划分可能会产生一个不同的代码,而签名的划分可能会比没有签名的划分慢。
基准
基准? 它们通常是毫无意义的,因为当你不经常重复相同的操作时,指令stream水线通常会使事情变得更快。 可以自由地考虑分裂缓慢,但是没有别的事情,当你超出基准testing时,你可能会注意到,由于stream水线化,在32位CPU上执行64位操作并不是很慢。
基准你自己的应用程序,不要相信微不足道的应用程序。 现代的CPU是相当棘手的,所以不相关的基准可以和将会在于。
你的问题在其环境中听起来很奇怪。 你使用了32位的time_t。 你需要额外的信息,什么意思是更多的位。 所以你不得不使用比int32更大的东西。 表演没有关系,对吧? select只需要说40位或者继续int64。 除非数以百万计的实例被存储,否则后者是一个明智的select。
正如其他人指出的,要知道真正的性能的唯一方法是用分析器来测量它(在一些总体样本中,一个简单的时钟就可以)。 所以只要继续测量。 将全局的time_t用法重新定义为64位,并修补less数预期实际时间的实例是不难的。
我敢打赌,除非你现在的time_t实例占用了至less几兆的内存,否则就是“不可估量的差别”。 在当前类似Intel的平台上,内核花费大部分时间等待外部存储器进入caching。 单个高速caching未命中延迟数百个周期。 什么使得指令上的1-tick差异是不可行的。 你的真实performance可能会下降,因为你现在的结构只适合一条caching线,而更大的一条需要两条。 如果你从来没有测量过目前的performance,你可能会发现,只要增加结构中某些成员的排列或交换顺序,就可以获得极大的提高。 或者打包(1)结构,而不是使用默认的布局…
加法/减法基本上变成两个循环,乘法和除法取决于实际的CPU。 一般的性能影响会相当低。
请注意,英特尔酷睿2支持EM64T。