我们的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)。
答案 0 :(得分:47)
哪些因素会影响这些操作的表现?可能是 编译器和编译器版本;但是操作系统还是 CPU make / model也影响这个?
主要是处理器架构(和模型 - 请阅读我在本节中提到处理器架构的模型)。编译器可能会有一些影响,但大多数编译器在这方面做得很好,因此处理器架构将比编译器具有更大的影响力。
操作系统没有任何影响(除了“如果你改变操作系统,你需要使用不同类型的编译器来改变编译器的功能” - 在某些情况下 - 但这可能是一个很小的影响)。
普通的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
答案 1 :(得分:9)
比以往任何时候都想知道在32位模式下进行64位数学运算......
在32位模式下使用64位数字时(即使代码编译为32位,在64位CPU上也是如此),它们存储为两个独立的32位数字,一个存储高位数字,另一个存储低位。这种影响取决于指令。 (tl; dr - 一般来说,在32位CPU上进行64位数学运算理论上要慢2倍,只要你不进行除法/模数,但实际上差异会更小(1.3倍会是我的猜测,因为通常程序不只是在64位整数上进行数学运算,而且由于流水线操作,程序中的差异可能要小得多。)
许多架构都支持所谓的carry flag。当加法结果溢出或减法结果不下溢时设置。这些位的行为可以通过长加法和长加法来显示。此示例中的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
认为是进位,如果设置了,它会在结果中加一。
类似地,如果未设置进位,则带进位的减法从结果中减去1。
此行为允许在整数上轻松实现任意大小的加法和减法。添加 x 和 y (假设它们是8位)的结果永远不会大于0x1FE
。如果您添加1
,则会获得0x1FF
。因此,9位足以表示任何8位加法的结果。如果您使用add
开始添加,然后使用adc
添加除初始值之外的任何位,则可以对您喜欢的任何大小的数据进行添加。
在32位CPU上添加两个64位值如下所示。
类似于减法。
这给出了2条指令,但是,由于instruction pipelinining,它可能比这慢,因为一个计算依赖于另一个完成,所以如果CPU没有其他任何事情要做,那么64位加法,CPU可能会等待第一次添加。
在x86上发生的情况是,imul
和mul
可以以溢出存储在 edx 寄存器中的方式使用。因此,将两个32位值相乘以获得64位值非常容易。这样的乘法是一条指令,但要使用它,其中一个乘法值必须存储在 eax 中。
无论如何,对于两个64位值相乘的更一般情况,可以使用以下公式计算它们(假设函数 r 删除超过32位的位)。
首先,很容易注意到结果的低32位将是乘法变量的低32位的乘法。这是由于一致关系。
a 1 ≡ b 1 (mod n )
a 2 ≡ b 2 (mod n )
a 1 a 2 ≡ b 1 < em> b 2 (mod n )
因此,任务仅限于确定更高的32位。要计算结果的高32位,应将以下值加在一起。
这提供了大约5条指令,但是由于x86中的寄存器数量相对有限(忽略了对体系结构的扩展),它们不能过多地利用流水线技术。如果要提高乘法速度,请启用SSE,因为这会增加寄存器的数量。
我不知道它是如何工作的,但它比加法,减法甚至乘法复杂得多。但它可能比64位CPU上的分区慢十倍。检查&#34;计算机程序设计的艺术,第2卷:研究数学算法&#34;,第257页了解更多细节,如果你能理解它(不幸的是我不能解释它)。
如果除以2的幂,请参考移位部分,因为基本上编译器可以优化除法(加上在移位有符号数之前加上最高有效位)。
考虑到这些操作是单位操作,这里没有什么特别的事情,只需按位操作两次。
有趣的是,x86实际上有一条指令来执行称为shld
的64位左移,它不是用零替换值的最低有效位,而是用不同寄存器的最高有效位替换它们。同样,使用shrd
指令进行右移的情况也是如此。这很容易使64位移位两个指令操作。
但是,这只是一个不断变换的情况。当一个班次不是一成不变的时候,事情变得越来越棘手,因为x86架构只支持0-31作为值的转变。除此之外的任何内容都是根据官方文档未定义的,并且在实践中,按位执行并且对值执行0x1F操作。因此,当移位值高于31时,一个值存储器被完全擦除(对于左移位,对于右移位,该低位字节,即更高的字节)。另一个获取已擦除的寄存器中的值,然后执行移位操作。结果,这取决于分支预测器做出良好的预测,并且因为需要检查值,所以有点慢。
__ builtin_popcount(lower)+ __builtin_popcount(更高)
我现在懒得完成答案。有人甚至使用那些吗?
加法,减法,乘法,或者,和,xor,左移,生成完全相同的代码。右移仅使用稍微不同的代码(算术移位与逻辑移位),但在结构上它是相同的。然而,除法确实会生成不同的代码,并且有符号除法可能比无符号除法慢。
基准?它们大多没有意义,因为当你不经常重复相同的操作时,指令流水线通常会导致事情变得更快。您可以随意考虑除法速度,但实际上没有其他内容,当您超出基准测试时,您可能会注意到由于流水线操作,在32位CPU上执行64位操作并不是很慢。
对您自己的应用程序进行基准测试,不要相信那些不能执行您的应用程序的微基准测试。现代CPU非常棘手,所以不相关的基准测试可以和将谎言。
答案 2 :(得分:2)
你的问题在它的环境中听起来很奇怪。使用最多占用32位的time_t。您需要其他信息,这意味着更多的比特。所以你被迫使用比int32更大的东西。性能是什么并不重要,对吧?只需说40位就可以选择,也可以继续使用int64。除非必须存储数百万个实例,否则后者是明智的选择。
正如其他人指出的那样,了解真实性能的唯一方法是使用分析器来测量它(在一些简单的时钟中会有一些大样本)。所以,继续前进并衡量。将time_t用法全局替换为typedef并将其重新定义为64位并修补预期有realtime_t的少数实例一定不难。
除非您当前的time_t实例占用至少几兆内存,否则我的赌注将是“无法衡量的差异”。在当前类似英特尔的平台上,内核大部分时间都在等待外部存储器进入缓存。单个高速缓存未命中会停止一百个周期。是什么让计算1-tick差异的指令变得不可行。您的实际性能可能会下降,因为您当前的结构只适合缓存行,而较大的需要两个。如果您从未测量过当前的性能,您可能会发现只需在结构中添加某些成员的一些对齐或交换顺序,就可以获得某些功能的极端加速。或者打包(1)结构而不是使用默认布局......
答案 3 :(得分:0)
加法/减法基本上分别变成两个周期,乘法和除法取决于实际的CPU。一般性能影响相当低。
请注意,Intel Core 2支持EM64T。