簡體   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