繁体   English   中英

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

[英]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位算术的代码。 但是如果这太慢了,我可能不得不使用更加差异化的方式来处理时间值,这可能会使软件更难维护。

我感兴趣的是:

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

我最感兴趣的是英特尔酷睿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中,添加操作称为addadc add支架以添加,而adc add支架。 它们之间的区别在于adc认为是进位,如果设置了,它会在结果中加一。

类似地,如果未设置进位,则使用进位减法从结果中减去1。

此行为允许在整数上轻松实现任意大小的加法和减法。 添加xy (假设它们是8位)的结果永远不会大于0x1FE 如果加1 ,则得到0x1FF 因此,9位足以表示任何8位加法的结果。 如果您使用add开始add ,然后使用adc添加除初始位之外的任何位,则可以对您喜欢的任何大小的数据进行添加。

在32位CPU上添加两个64位值如下。

  1. B的前32位添加到前32位。
  2. 添加与进位后32位b更高的32位。

类似地用于减法。

这给出了2个指令,但是,由于指令管道 ,它可能比这慢,因为一个计算依赖于另一个计算完成,所以如果CPU除了64位之外没有任何其他操作,CPU可能会等待第一次加入。

乘法

在x86上imul如此, imulmul可以以溢出存储在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位,应将以下值加在一起。

  • 低32位的高32位乘法(CPU可以存储在edx中的溢出)
  • 第一个变量的高32位多用第二个变量的低32位
  • 第一个变量的低32位乘以第二个变量的高32位

这给出了大约5条指令,但是由于x86中的寄存​​器数量相对有限(忽略了对体系结构的扩展),它们不能过多地利用流水线技术。 如果要提高乘法速度,请启用SSE,因为这会增加寄存器的数量。

Division / Modulo(两者在实现上类似)

我不知道它是如何工作的,但它比加法,减法甚至乘法复杂得多。 然而,它可能比64位CPU上的分区慢十倍。 如果您能理解它,请查看“计算机编程艺术,第2卷:精神数学算法”,第257页,以获取更多详细信息(遗憾的是,我不能以某种方式解释它)。

如果除以2的幂,请参考移位部分,因为基本上编译器可以优化除法(加上在移位有符号数之前加上最高有效位)。

和/或/ XOR

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

向左/向右移动

有趣的是,x86实际上有一个执行64位左移的指令,称为shld ,它不是用零替换值的最低有效位,而是用不同寄存器的最高有效位替换它们。 类似地,使用shrd指令进行右移也是如此。 这很容易使64位移位两个指令操作。

然而,这只是不断变化的情况。 当一个班次不是一成不变的时候,事情变得越来越棘手,因为x86架构只支持0-31作为值的转变。 除此之外的任何内容都是根据官方文档未定义的,并且在实践中,按位执行并且对值执行0x1F操作。 因此,当一个移位值高于31时,一个值存储器被完全擦除(对于左移,这是较低的字节,对于右移,这是较高的字节)。 另一个获取已擦除的寄存器中的值,然后执行移位操作。 结果,这取决于分支预测器做出良好的预测,并且因为需要检查值而稍微慢一点。

__builtin_popcount [11]

__builtin_popcount(lower)+ __builtin_popcount(更高)

其他内置组件

我现在懒得完成答案。 有没有人甚至使用那些?

未签名vs签名

加法,减法,乘法,或者,和,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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM