[英]what is the performance impact of using int64_t instead of int32_t on 32-bit systems?
我们的C ++库目前使用time_t来存储时间值。 我开始在某些地方需要亚秒精度,因此无论如何都需要更大的数据类型。 此外,在某些地方解决2038年问题可能会有所帮助。 所以我正在考虑完全切换到具有基础int64_t值的单个Time类,以替换所有位置的time_t值。
现在,我想知道在32位操作系统或32位CPU上运行此代码时这种更改对性能的影响。 IIUC编译器将生成使用32位寄存器执行64位算术的代码。 但是如果这太慢了,我可能不得不使用更加差异化的方式来处理时间值,这可能会使软件更难维护。
我感兴趣的是:
我最感兴趣的是英特尔酷睿2系统上Linux 2.6(RHEL5,RHEL6)上的g ++ 4.1和4.4; 但了解其他系统(如Sparc Solaris + Solaris CC,Windows + MSVC)的情况也很好。
哪些因素影响这些业务的表现? 可能是编译器和编译器版本; 但操作系统或CPU制造商/型号是否也会影响这一点?
主要是处理器架构(和模型 - 请阅读我在本节中提到处理器架构的模型)。 编译器可能会有一些影响,但大多数编译器在这方面做得很好,因此处理器架构将比编译器具有更大的影响力。
操作系统将没有任何影响(除了“如果你改变操作系统,你需要使用不同类型的编译器,在某些情况下改变编译器的功能” - 但这可能是一个很小的影响)。
普通的32位系统是否会使用现代CPU的64位寄存器?
这是不可能的。 如果系统处于32位模式,它将充当32位系统,额外的32位寄存器是完全不可见的,就像系统实际上是“真正的32位系统”一样。
在32位模拟时哪些操作会特别慢? 或者几乎没有减速?
加法和减法更糟糕,因为这些必须按两个操作的顺序完成,而第二个操作要求第一个完成 - 如果编译器只对独立数据产生两个添加操作,则不是这种情况。
如果输入参数实际上是64位,那么多重复制会变得更糟 - 例如,2 ^ 35 * 83比2 ^ 31 * 2 ^ 31差。 这是因为处理器可以很好地生成32位32位乘法到64位结果 - 大约5-10个时钟周期。 但64 x 64位乘法需要相当多的额外代码,因此需要更长时间。
除法是一个类似的乘法问题 - 但是这里可以在一侧采用64位输入,将其除以32位值并获得32位值。 由于很难预测这种情况何时起作用,因此64位除法可能几乎总是很慢。
数据也将占用缓存空间的两倍,这可能会影响结果。 并且作为类似的结果,一般的分配和传递数据将花费两倍于最小值,因为有两倍的数据可以操作。
编译器还需要使用更多寄存器。
是否存在在32位系统上使用int64_t / uint64_t的任何现有基准测试结果?
可能,但我不知道。 即使有,也只对你有所帮助,因为操作组合对操作速度至关重要。
如果性能是您的应用程序的重要部分,那么对您的代码(或其代表性部分)进行基准测试。 如果你的代码在相同的情况下变得更慢或更快,那么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年结果 - 由于64位模式下SSE指令的使用不同,这些结果略微乐观,但在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
正如您所看到的,添加和乘法并没有那么糟糕。 分部变得非常糟糕。 有趣的是,矩阵的添加完全没有太大区别。
并且它在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位数学比你想知道的更多......
当您在32位模式下使用64位数字时(即使在64位CPU上,如果代码编译为32位),它们也会存储为两个独立的32位数字,一个存储一个数字的高位,以及另一个存储低位。 这种影响取决于指令。 (tl; dr - 一般来说,在32位CPU上进行64位数学运算理论上要慢2倍,只要你不分/模,但实际上差异会更小(1.3x将是我的因为通常程序不只是对64位整数进行数学运算,而且由于流水线操作,所以程序中的差异可能要小得多)。
许多架构支持所谓的进位标志 。 当加法结果溢出或减法结果不下溢时设置。 这些位的行为可以通过长加法和长加法来显示。 此示例中的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
add
支架。 它们之间的区别在于adc
认为是进位,如果设置了,它会在结果中加一。
类似地,如果未设置进位,则使用进位减法从结果中减去1。
此行为允许在整数上轻松实现任意大小的加法和减法。 添加x和y (假设它们是8位)的结果永远不会大于0x1FE
。 如果加1
,则得到0x1FF
。 因此,9位足以表示任何8位加法的结果。 如果您使用add
开始add
,然后使用adc
添加除初始位之外的任何位,则可以对您喜欢的任何大小的数据进行添加。
在32位CPU上添加两个64位值如下。
类似地用于减法。
这给出了2个指令,但是,由于指令管道 ,它可能比这慢,因为一个计算依赖于另一个计算完成,所以如果CPU除了64位之外没有任何其他操作,CPU可能会等待第一次加入。
在x86上imul
如此, imul
和mul
可以以溢出存储在edx寄存器中的方式使用。 因此,将两个32位值相乘以获得64位值非常容易。 这样的乘法是一条指令,但是为了利用它,乘法值之一必须存储在eax中 。
无论如何,对于两个64位值相乘的更一般情况,可以使用以下公式计算它们(假设函数r移除超过32位的位)。
首先,很容易注意到结果的低32位将乘以低位32位的乘法变量。 这是由于一致关系。
1≡b 1(MOD N)
2≡B 2(MOD N)
A 1 A 2≡b 1 B 2(MOD N)
因此,任务仅限于确定较高的32位。 要计算结果的高32位,应将以下值加在一起。
这给出了大约5条指令,但是由于x86中的寄存器数量相对有限(忽略了对体系结构的扩展),它们不能过多地利用流水线技术。 如果要提高乘法速度,请启用SSE,因为这会增加寄存器的数量。
我不知道它是如何工作的,但它比加法,减法甚至乘法复杂得多。 然而,它可能比64位CPU上的分区慢十倍。 如果您能理解它,请查看“计算机编程艺术,第2卷:精神数学算法”,第257页,以获取更多详细信息(遗憾的是,我不能以某种方式解释它)。
如果除以2的幂,请参考移位部分,因为基本上编译器可以优化除法(加上在移位有符号数之前加上最高有效位)。
考虑到这些操作是单位操作,这里没有什么特别的事情发生,只需按位操作两次。
有趣的是,x86实际上有一个执行64位左移的指令,称为shld
,它不是用零替换值的最低有效位,而是用不同寄存器的最高有效位替换它们。 类似地,使用shrd
指令进行右移也是如此。 这很容易使64位移位两个指令操作。
然而,这只是不断变化的情况。 当一个班次不是一成不变的时候,事情变得越来越棘手,因为x86架构只支持0-31作为值的转变。 除此之外的任何内容都是根据官方文档未定义的,并且在实践中,按位执行并且对值执行0x1F操作。 因此,当一个移位值高于31时,一个值存储器被完全擦除(对于左移,这是较低的字节,对于右移,这是较高的字节)。 另一个获取已擦除的寄存器中的值,然后执行移位操作。 结果,这取决于分支预测器做出良好的预测,并且因为需要检查值而稍微慢一点。
__builtin_popcount(lower)+ __builtin_popcount(更高)
我现在懒得完成答案。 有没有人甚至使用那些?
加法,减法,乘法,或者,和,xor,左移,生成完全相同的代码。 右移仅使用稍微不同的代码(算术移位与逻辑移位),但在结构上它是相同的。 然而,除法确实会生成不同的代码,并且有符号除法可能比无符号除法慢。
基准? 它们大多没有意义,因为当你不经常重复相同的操作时,指令流水线通常会导致事情变得更快。 您可以随意考虑除法,但实际上没有其他内容,当您超出基准测试时,您可能会注意到由于流水线操作,在32位CPU上执行64位操作并不是很慢。
对您自己的应用程序进行基准测试,不要相信不能执行您的应用程序的微基准测试。 现代CPU非常棘手,所以不相关的基准测试可以而且将会存在。
你的问题在它的环境中听起来很奇怪。 使用最多占用32位的time_t。 您需要其他信息,这意味着更多的比特。 所以你被迫使用比int32更大的东西。 性能是什么并不重要,对吧? 只需说40位就可以选择,也可以继续使用int64。 除非必须存储数百万个实例,否则后者是明智的选择。
正如其他人指出的那样,了解真实性能的唯一方法是使用分析器来测量它(在一些简单的时钟中可以做一些大样本)。 所以,继续前进并衡量。 将time_t用法全局替换为typedef并将其重新定义为64位并修补预期有realtime_t的少数实例一定不难。
除非你当前的time_t实例占用至少几兆内存,否则我的赌注将是“无法衡量的差异”。 在当前类似英特尔的平台上,内核大部分时间都在等待外部存储器进入缓存。 单个高速缓存未命中会停止一百个周期。 是什么让计算1-tick差异的指令变得不可行。 您的实际性能可能会下降,因为您当前的结构只适合缓存行,而较大的需要两个。 如果您从未测量过当前的性能,您可能会发现只需在结构中添加某些成员的一些对齐或交换顺序,就可以获得某些功能的极端加速。 或者打包(1)结构而不是使用默认布局......
加法/减法基本上分别变为两个周期,乘法和除法取决于实际的CPU。 一般性能影响将相当低。
请注意,Intel Core 2支持EM64T。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.