53

私たちの C++ ライブラリは現在、時間値を保存するために time_t を使用しています。一部の場所では 1 秒未満の精度が必要になり始めているため、いずれにせよ、より大きなデータ型が必要になります。また、場所によっては 2038 年問題を回避するのに役立つ場合もあります。そのため、基になる int64_t 値を持つ単一の Time クラスに完全に切り替えて、すべての場所で time_t 値を置き換えることを考えています。

このコードを 32 ビット オペレーティング システムまたは 32 ビット CPU で実行した場合の、このような変更によるパフォーマンスへの影響について疑問に思っています。IIUC コンパイラは、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 など) の状況についても知っておくとよいでしょう。

4

4 に答える 4

47

これらの操作のパフォーマンスに影響を与える要因は何ですか? おそらくコンパイラとコンパイラのバージョンです。しかし、オペレーティングシステムまたはCPUのメーカー/モデルもこれに影響しますか?

主にプロセッサ アーキテクチャ (およびモデル - このセクションでプロセッサ アーキテクチャについて言及しているモデルを参照してください)。コンパイラは多少の影響を与える可能性がありますが、ほとんどのコンパイラはこれにかなりうまく対応しているため、プロセッサ アーキテクチャはコンパイラよりも大きな影響を与えます。

オペレーティングシステムはまったく影響を与えません(「OSを変更する場合、コンパイラの動作を変更する別のタイプのコンパイラを使用する必要がある」場合を除きますが、それはおそらく小さな影響です)。

通常の 32 ビット システムは最新の CPU の 64 ビット レジスタを使用しますか?

これは不可能です。システムが 32 ビット モードの場合、システムは 32 ビット システムとして動作し、システムが実際に「真の 32 ビット システム」であるかのように、レジスタの余分な 32 ビットは完全に見えなくなります。 .

32 ビットでエミュレートすると特に遅くなる操作はどれですか? または、減速がほとんどないのはどれですか?

加算と減算は、2 つの演算のシーケンスで実行する必要があり、2 番目の演算では最初の演算が完了している必要があるため、さらに悪化します。これは、コンパイラが独立したデータに対して 2 つの加算演算を生成するだけの場合には当てはまりません。

入力パラメーターが実際に 64 ビットの場合、乗算はさらに悪化します。たとえば、2^35 * 83 は 2^31 * 2^31 よりも悪化します。これは、プロセッサが 32 x 32 ビットの乗算を 64 ビットの結果にかなりうまく生成できるためです (約 5 ~ 10 クロックサイクル)。ただし、64 x 64 ビットの乗算にはかなりの追加コードが必要なため、時間がかかります。

割り算は掛け算と似た問題ですが、ここでは一方に 64 ビットの入力を取り、それを 32 ビットの値で割り、32 ビットの値を出力しても問題ありません。これがいつ機能するかを予測するのは難しいため、64 ビットの除算はほぼ常に低速です。

また、データは 2 倍のキャッシュ スペースを必要とするため、結果に影響を与える可能性があります。同様の結果として、操作するデータが 2 倍になるため、一般的な割り当てとデータの受け渡しには、最小で 2 倍の時間がかかります。

コンパイラは、より多くのレジスタを使用する必要もあります。

32 ビット システムで int64_t/uint64_t を使用するための既存のベンチマーク結果はありますか?

おそらくですが、私は何も知りません。たとえあったとしても、操作の組み合わせは操作の速度にとって非常に重要であるため、あなたにとってはある程度意味があるだけです。

パフォーマンスがアプリケーションの重要な部分である場合は、コード (またはその代表的な部分) をベンチマークしてください。Benchmark X の結果が 5%、25%、または 103% 遅いかどうかは問題ではありません。同じ状況下で、コードの速度がまったく異なる場合、または速度がまったく異なる場合です。

このパフォーマンスへの影響について自分の経験を持っている人はいますか?

64 ビット アーキテクチャ用に 64 ビット整数を使用するコードを再コンパイルしたところ、パフォーマンスが大幅に改善されたことがわかりました。一部のコードでは 25% も改善されています。

OS を同じ OS の 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 年の編集、更​​新: SSE の有無にかかわらず、コンパイラの 32 ビット モードと 64 ビット モードでの 4 つのバリアント。

私は通常、最近の通常のコンパイラとして clang++ を使用しています。g++ でコンパイルしてみました (ただし、マシンを更新したため、上記とは異なるバージョンであり、CPU も異なります)。g++ は no-sse バージョンを 64 ビットでコンパイルできなかったため、その意味がわかりませんでした。(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
于 2013-05-30T16:52:54.883 に答える
10

32 ビット モードで 64 ビットの計算を行うことについて、あなたが知りたいと思っていた以上のことが...

32 ビット モードで 64 ビット数値を使用する場合 (コードが 32 ビット用にコンパイルされている場合は 64 ビット CPU でも)、それらは 2 つの別個の 32 ビット数値として格納され、1 つは数値の上位ビットを格納し、もう1つは下位ビットを格納します。この影響は、命令によって異なります。(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 には通常 2 つの別々の加算演算と減算演算があります。x86 では、加算演算は and と呼ばaddadcます。addは足し算を表し、adcキャリー付きの足し算は足し算を表します。これらの違いはadc、キャリー ビットを考慮し、設定されている場合は結果に 1 を追加することです。

同様に、キャリーを使用した減算では、キャリー ビットが設定されていない場合、結果から 1 が減算されます。

この動作により、整数に対して任意のサイズの加算と減算を簡単に実装できます。xyを加算した結果(これらが 8 ビットであると仮定) は、 より大きくなることはありません0x1FE。を追加する1と、 が得られ0x1FFます。したがって、8 ビット加算の結果を表すには 9 ビットで十分です。で足し算を始め、 でadd最初のビットを超えたビットを足し算すればadc、好きなサイズのデータ​​を足し算できます。

32 ビット CPU で 2 つの 64 ビット値を加算すると、次のようになります。

  1. bの最初の 32 ビットをaの最初の 32 ビットに加算します。
  2. bの後ろの32ビットをaの後ろの 32 ビットにキャリー付きで加算します。

引き算の類推。

これにより2つの命令が得られますが、命令のパイプライン化により、1つの計算が他の計算の完了に依存するため、それよりも遅くなる可能性があります.最初の追加が行われます。

乗算

これは x86 で発生し、オーバーフローがedxレジスタに格納されるような方法で使用できますimul。したがって、2 つの 32 ビット値を乗算して 64 ビット値を取得するのは非常に簡単です。このような乗算は 1 つの命令ですが、それを利用するには、乗算値の 1 つをeaxに格納する必要があります。mul

とにかく、2 つの 64 ビット値の乗算のより一般的なケースでは、次の式を使用して計算できます (関数rが 32 ビットを超えるビットを削除すると仮定します)。

まず、結果の下位 32 ビットが、乗算された変数の下位 32 ビットの乗算であることは容易にわかります。これは合同関係によるものです。

a 1b 1 (mod n )
a 2b 2 (mod n )
a 1 a 2b 1 b 2 (mod n )

したがって、タスクは上位 32 ビットの決定のみに限定されます。結果の上位 32 ビットを計算するには、次の値を加算する必要があります。

  • 両方の下位 32 ビットの乗算の上位 32 ビット (CPU がedxに格納できるオーバーフロー)
  • 最初の変数の上位 32 ビットと 2 番目の変数の下位 32 ビットの乗算
  • 最初の変数の下位 32 ビットに 2 番目の変数の上位 32 ビットを乗算

これにより約 5 つの命令が得られますが、x86 ではレジスタの数が比較的限られているため (アーキテクチャの拡張を無視して)、パイプライン処理をあまり活用できません。レジスタの数が増えるため、乗算の速度を向上させたい場合は、SSE を有効にします。

除算/モジュロ (どちらも実装は似ています)

それがどのように機能するかはわかりませんが、足し算、引き算、さらには掛け算よりもはるかに複雑です。ただし、64 ビット CPU での除算よりも 10 倍遅くなる可能性があります。詳細については、"Art of Computer Programming, Volume 2: Seminumerical Algorithms" の 257 ページを確認してください (残念ながら、説明できる方法ではありません)。

2 の累乗で除算する場合は、シフト セクションを参照してください。これは、基本的にコンパイラが除算を最適化できるためです (さらに、符号付き数値をシフトする前に最上位ビットを追加します)。

または/および/Xor

これらの操作が単一ビット操作であることを考慮すると、ここでは特別なことは何も起こらず、ビットごとの操作が 2 回行われるだけです。

左右シフト

興味深いことに、x86 には と呼ばれる 64 ビットの左シフトを実行する命令が実際にあり、shld値の最下位ビットをゼロに置き換える代わりに、別のレジスタの最上位ビットに置き換えます。shrd同様に、右シフト命令の場合も同様です。これにより、簡単に 64 ビット シフトが 2 命令操作になります。

ただし、これは一定のシフトの場合にのみ当てはまります。x86 アーキテクチャは値として 0 ~ 31 のシフトのみをサポートするため、シフトが一定でない場合はさらに複雑になります。それを超えるものは、公式ドキュメントによると未定義であり、実際には、値に対してビット単位および 0x1F での操作が実行されます。したがって、シフト値が 31 より大きい場合、値ストレージの 1 つが完全に消去されます (左シフトの場合は下位バイト、右シフトの場合は上位バイト)。もう1つは、消去されたレジスタにあった値を取得し、シフト操作が実行されます。結果として、これは適切な予測を行うために分岐予測子に依存し、値をチェックする必要があるため少し遅くなります。

__builtin_popcount[ll]

__builtin_popcount(下位) + __builtin_popcount(上位)

その他のビルトイン

この時点で答えを終えるのが面倒です。誰もそれらを使用していますか?

署名なし vs 署名済み

加算、減算、乗算、および xor、左シフトは、まったく同じコードを生成します。右シフトはわずかに異なるコード (算術シフトと論理シフト) を使用するだけですが、構造的には同じです。ただし、除算では異なるコードが生成される可能性が高く、符号付き除算は符号なし除算よりも遅くなる可能性があります。

ベンチマーク

ベンチマーク?同じ操作を常に繰り返さないと、通常、命令のパイプライン化によって処理が高速化されるため、これらはほとんど意味がありません。除算が遅いと考えて構いませんが、実際にはそうではありません。ベンチマークの外に出ると、パイプライン処理のおかげで、32 ビット CPU で 64 ビット操作を実行してもまったく遅くないことに気付くかもしれません。

独自のアプリケーションをベンチマークします。アプリケーションが行うことを行わないマイクロ ベンチマークを信頼しないでください。最新の CPU は非常に扱いにくいため、無関係なベンチマークが嘘をつく可能性があります

于 2016-01-10T14:18:57.337 に答える
2

あなたの質問は、その環境ではかなり奇妙に聞こえます。32 ビットを使用する time_t を使用します。追加情報が必要です。つまり、より多くのビットが必要です。したがって、int32 よりも大きなものを使用する必要があります。性能なんて関係ないですよね?選択肢は、単に 40 ビットを使用するか、int64 を使用するかの間です。何百万ものインスタンスを保存する必要がない限り、後者は賢明な選択です。

他の人が指摘したように、真のパフォーマンスを知る唯一の方法は、プロファイラーで測定することです (一部の総サンプルでは、​​単純なクロックで十分です)。測定してみましょう。time_t の使用法を typedef にグローバルに置き換えて 64 ビットに再定義し、real time_t が予期されていたいくつかのインスタンスにパッチを適用することは難しくありません。

現在の time_t インスタンスが少なくとも数メガのメモリを占有しない限り、私の賭けは「測定不能な違い」にあります。現在のインテルのようなプラットフォームでは、コアはほとんどの時間を外部メモリがキャッシュに入るのを待つことに費やしています。1 つのキャッシュ ミスが数百サイクルの間ストールします。命令の 1 ティック差の計算が実行不可能になる理由。実際のパフォーマンスが低下する可能性があるのは、現在の構造が 1 つのキャッシュ ラインにちょうど収まり、大きいものには 2 つ必要な場合などです。また、現在のパフォーマンスを測定したことがない場合は、構造体の一部のメンバーの整列または交換順序を追加するだけで、一部の関数の速度が大幅に向上することに気付くかもしれません。または、デフォルトのレイアウトを使用する代わりに構造を pack(1) します...

于 2013-05-30T19:35:45.693 に答える