在32位系统上使用int64_t代替int32_t会对性能产生什么影响?

53

我们的C++库目前使用time_t来存储时间值。我开始需要一些亚秒精度,因此在某些地方将需要更大的数据类型。此外,为了解决2038年问题,在某些地方切换到具有基础int64_t值的单个Time类可能会很有用,以替换所有位置的time_t值。

现在我在想,在32位操作系统或32位CPU上运行此代码会对性能产生什么影响。如果我理解正确,编译器将生成使用32位寄存器执行64位算术的代码。但是,如果这太慢了,则可能必须使用更多样化的方法来处理时间值,这可能会使软件更难以维护。

我感兴趣的是:

  • 哪些因素影响这些操作的性能?可能是编译器和编译器版本;但操作系统或CPU型号/制造商是否也会产生影响?普通的32位系统是否会使用现代CPU的64位寄存器?
  • 在32位模拟时,哪些操作尤其缓慢?或者哪些操作几乎没有减速?
  • 是否有关于在32位系统上使用int64_t/uint64_t的任何现有基准测试结果?
  • 是否有任何人对此性能影响有自己的经验?

我最感兴趣的是在Intel Core 2系统上的Linux 2.6(RHEL5,RHEL6)上使用g++ 4.1和4.4,但也很想了解其他系统(如Sparc Solaris + Solaris CC,Windows + MSVC)的情况。


7
只有仔细的分析才能确定是这样还是那样。 - Sergey Kalinichenko
编写两个小例子,编译它们并比较汇编代码。我相信这可能低于分析工具的检测范围,比较汇编代码是最好的方法。 - andre
@andre 这是一个不同的问题,但由于这是关于性能的,所以并不相关。重要的是时间安排。 - David Heffernan
3
在 David H 和 @andre 的基础上,对于现代系统而言,仅仅查看指令并不足以确定代码的执行时间。你可能会发现,尽管指令序列看起来相同(包含相同数量的相同指令,只是使用了不同的寄存器),但它们运行速度却非常不同,例如因为一个依赖于前一个操作的结果,而另一个则不依赖。或者缓存命中/未命中会影响结果,或者其他类似的因素。 - Mats Petersson
3
你考虑使用双精度浮点数吗?如果你只用它来存储整数,那么实际上你将得到一个53位的整数,这比你现在拥有的32位要大得多。 - tzs
显示剩余5条评论
4个回答

47
哪些因素影响这些操作的性能? 可能是编译器和编译器版本; 但操作系统或CPU型号是否也会产生影响呢?
主要是处理器架构(和型号-在本节中提到处理器架构时请阅读型号)。编译器可能会有一些影响,但大多数编译器在此方面表现良好,因此处理器架构的影响将比编译器更大。
操作系统将完全没有影响(除了“如果您更改操作系统,则需要使用不同类型的编译器,这会改变编译器所做的事情”在某些情况下 - 但这可能是一个小效应)。
普通32位系统会使用现代CPU的64位寄存器吗?
这是不可能的。如果系统处于32位模式下,它将作为32位系统运行,寄存器的额外32位完全不可见,就像它实际上是“真正的32位系统”一样。
在32位模拟时,哪些操作将特别缓慢?或者哪些几乎不会减速?
加法和减法,由于这些必须按两个操作序列执行,并且第二个操作需要第一个操作已完成-如果编译器只是在独立数据上生成两个添加操作,则情况并非如此。
如果输入参数实际上是64位,则乘法将变得更糟 - 因此,2 ^ 35 * 83比2 ^ 31 * 2 ^ 31更糟。这是由于处理器可以将32 x 32位乘法产生为64位结果-大约需要5-10个时钟周期。但是,64 x 64位乘法需要相当多的额外代码,因此需要更长时间。
除法与乘法类似-但在这里,从一侧取一个64位输入,将其除以32位值并获得32位值是可以的。由于很难预测何时会起作用,所以64位除法几乎总是很慢的。
数据也将占用两倍的缓存空间,这可能会影响结果。同样的后果是,通常分配和传递数据将至少需要两倍的时间,因为有两倍的数据要操作。
编译器还需要使用更多寄存器。
是否有使用32位系统上的int64_t / uint64_t的现有基准结果?
可能有,但我不知道。即使有,它对您来说也仅略具意义,因为操作的混合方式极其关键。
如果性能是您的应用程序中重要的部分,那么请对您的代码(或其中一部分代表性代码)进行基准测试。如果在相同情况下,您的代码比基准测试X慢或快了完全不同的数量级,那么基准测试X给出的5%、25%或103%的结果都不重要。

有人有关于性能影响的个人经验吗?

我重新编译了一些使用64位整数的代码,用于64位架构,并发现某些代码段的性能提高了相当多——在某些代码段上提高了高达25%。
将操作系统更改为同一操作系统的64位版本会有所帮助吗?
编辑:
因为我喜欢找出这些事情的区别,所以我写了一些代码,并使用一些原始模板(我还在学习模板-模板并不是我的最热门话题,我必须说-给我一点位操作和指针算术,我通常会做得很好……)
以下是我编写的代码,试图复制几个常见函数:
#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位是否更快: 使用相同的编译器选项,只需将-m32替换为-m64 - 是的,快了很多:

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位中无法编译无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

2
告诉我,x32 的代码段选择器中设置了哪些位?更具体地说,第 53 位的值是多少?它被设置了!换句话说,x32 并不是真正的 32 位模式。它使用 64 位寄存器和 64 位模式,但只有前后各 2GB 的虚拟地址空间和 32 位指针 [符号扩展为 64 位]。 - Mats Petersson
@delnan:我现在添加了一个小型自制基准测试,展示了32位和64位整数计算在32位和64位代码构建中的性能。 - Mats Petersson
Mats,感谢您提供的出色基准测试!这至少为32位系统的性能影响提供了一些基本范围。目前完全转换为本机64位还不太可能,因为我们还必须支持现有系统。 - oliver
99.9%的把握,“现有系统”在64位环境下可以正常运行。 - Mats Petersson
1
我觉得32位代码在64位模式下的性能如此之快,有点让人怀疑。我可以想象你的(琐碎)循环正在进行向量化处理,但只有x64默认支持向量化处理。向量化性能确实值得单独分析。理想情况下,您希望您的基准测试最初避免可向量化,并且您还希望对循环展开不太敏感(您正在基准测试加法,因此额外的递增会产生影响)。 - Eamon Nerbonne
显示剩余9条评论

9

关于在32位模式下进行64位数学运算的更多信息...

当您在32位模式下(即使在64位CPU上编译代码为32位)使用64位数字时,它们将作为两个分开的32位数字存储,一个存储数字的高位,另一个存储数字的低位。这取决于指令的影响。(tl;dr-通常情况下,在32位CPU上进行64位数学运算理论上会慢2倍,只要您不进行除法/取模,但实际上差异会更小(我猜测是1.3倍),因为通常程序不仅仅在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代表带进位的加法。它们之间的区别在于adc考虑进位标志位,如果设置了进位标志位,则将1添加到结果中。
类似地,如果未设置进位标志位,则带进位的减法会从结果中减去1。
这种行为允许轻松实现任意大小的整数加法和减法。假设对8位整数xy进行加法运算,结果永远不会超过0x1FE。如果加1,则得到0x1FF。因此,9位就足以表示任何8位加法的结果。如果您从add开始进行加法,然后使用adc添加除初始位以外的任何位,您可以对任何数据大小执行加法。
在32位CPU上执行两个64位值的加法如下:
1. 将b的前32位与a的前32位相加。 2. 将b的后32位与a的后32位相加,并带上进位。
减法同理。
这样可以得到2个指令,但是由于指令流水线的原因,速度可能会比那慢,因为一个计算取决于另一个完成,因此如果CPU没有其他事情要做,只执行64位加法,则CPU可能需要等待第一个加法完成。
乘法 在x86中,imulmul可以用一种方式使用,使得溢出存储在edx寄存器中。因此,将两个32位值相乘以获得64位值非常容易。这种乘法是一条指令,但是要利用它,其中一个乘法值必须存储在eax中。
无论如何,对于更一般的情况,可以使用以下公式(假设函数r移除了超过32位的位)来计算两个64位值的乘积。
首先,很容易注意到结果的低32位将是乘积变量的低32位的乘积。这是由于同余关系造成的。

a1b1 (mod n)
a2b2 (mod n)
a1a2b1b2 (mod n)

因此,任务仅限于确定更高的32位。要计算结果的高32位,需要将以下值相加。

  • 两个低32位数的乘积的高32位(CPU可以存储在edx中的溢出)
  • 第一个变量的高32位与第二个变量的低32位相乘的高32位
  • 第一个变量的低32位与第二个变量的高32位相乘的低32位

这大约需要5条指令,但是由于x86中寄存器数量相对有限(忽略对架构的扩展),它们无法充分利用流水线技术。如果想提高乘法速度,请启用SSE,因为这会增加寄存器的数量。

除法/模(两者实现类似)

我不知道它是如何工作的,但它比加法、减法甚至乘法复杂得多。然而,在64位CPU上,它可能比除法慢十倍。如果你能理解,请查看"计算机程序设计艺术,第2卷:半数值算法",第257页以获取更多详细信息(不幸的是,我无法以一种我能够解释的方式理解它)。

如果您要除以2的幂次方,请参考移位部分,因为这就是编译器可以将除法优化为的内容(对于带符号数,还需要在移位之前将最高有效位添加)。

或/与/Xor

考虑到这些操作是单比特操作,没有什么特别的事情发生,只是进行了两次按位操作。

左/右移

有趣的是,x86实际上有一条指令可以执行64位左移,称为shld,它不是将值的最低有效位替换为零,而是用另一个寄存器的最高有效位替换它们。同样,对于右移也是这样的情况,使用shrd指令。这将轻松地使64位移位成为一项两条指令的操作。

然而,这只适用于常数移位的情况。当移位不是常数时,情况就会变得更加棘手,因为x86体系结构仅支持将0-31作为值的移位。超过31的任何值根据官方文档均未定义,在实践中,对值执行了0x1F的按位与运算。因此,当移位值大于31时,一个值存储器完全被抹去(对于左移,较低字节被抹去,对于右移,较高字节被抹去)。另一个值获得在被抹去的寄存器中的值,然后执行移位操作。这取决于分支预测器进行良好的预测,而且由于需要检查值,速度会慢一些。

__builtin_popcount [ll]

__builtin_popcount(lower) + __builtin_popcount(higher)

其他内置函数

我现在有点懒得完成回答了。还有人在使用这些吗?

无符号 vs 有符号

加法、减法、乘法、或、与、异或和左移生成完全相同的代码。右移仅使用略有不同的代码(算术移位与逻辑移位),但在结构上是相同的。然而,除法很可能生成不同的代码,有符号除法可能比无符号除法慢。

基准测试

基准测试?它们通常是毫无意义的,因为指令流水线通常会导致在不不断重复相同操作时速度更快。虽然可以认为除法很慢,但其他操作并不慢,而且当你超出基准测试范围后,你可能会注意到,在32位CPU上进行64位操作并不慢,这是由于指令流水线的原因。

对您自己的应用程序进行基准测试,不要相信不执行您应用程序所执行操作的微基准测试。现代CPU非常棘手,因此不相关的基准测试可能会误导您。


2
你的问题在其环境中听起来有些奇怪。你使用了使用32位的time_t,需要更多的信息,这意味着需要更多的位数。因此,你被迫使用比int32更大的东西。性能如何并不重要,对吧?选择将在只使用40位或继续使用int64之间进行。除非必须存储数百万个实例,否则后者是明智的选择。
正如其他人指出的那样,了解真正的性能唯一的方法是使用分析器进行测量(在某些粗略的样本中,一个简单的时钟就可以完成)。所以,继续测量。全局替换你的time_t使用typedef并重新定义为64位,并修补一些实际需要real time_t的实例,这应该不难。
我敢打赌,除非你当前的time_t实例占用至少几兆字节的内存,否则差别是无法测量的。在当前类似Intel的平台上,核心大部分时间都在等待外部内存进入缓存。一个缓存未命中会暂停数百个周期。这使得计算指令中1个时钟周期的差异变得不可行。由于你当前的结构恰好适合缓存行,而更大的结构需要两个缓存行,这可能会导致你的实际性能下降。如果你从未测量过当前的性能,你可能会发现通过为某些函数添加一些对齐或交换结构中某些成员的顺序,或将结构压缩(pack(1))而不使用默认布局,可以获得极高的加速。

我并不需要在所有地方都使用额外的精度 - 有些算法可以使用time_t精度正常运行。问题是我是否应该在我的代码中使用两种不同的时间类型(作为性能改进),还是可以一直使用int64_t,即使在不需要额外精度的地方。但是,是的,我会设置一些真实场景的基准测试来看看这是否真的很重要。 - oliver

0

加减法基本上变成了两个循环,乘法和除法取决于实际的CPU。总体性能影响将会相当低。

请注意,英特尔Core 2支持EM64T。


英特尔酷睿2是32位处理器吗?不,它是64位处理器。 - Dan
但是运行在其上的系统可能是32位的。据我所知,程序也不会使用64位指令,因为操作系统不支持64位,并且编译器必须假定32位ABI和指令集。 - user395760

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接