14

I am taking a look at large matrix multiplication and ran the following experiment to form a baseline test:

  1. Randomly generate two 4096x4096 matrixes X, Y from std normal (0 mean, 1 stddev).
  2. Z = X*Y
  3. Sum elements of Z (to make sure they are accessed) and output.

Here is the naïve C++ implementatation:

#include <iostream>
#include <algorithm>

using namespace std;

int main()
{
    constexpr size_t dim = 4096;

    float* x = new float[dim*dim];
    float* y = new float[dim*dim];
    float* z = new float[dim*dim];

    random_device rd;
    mt19937 gen(rd());
    normal_distribution<float> dist(0, 1);

    for (size_t i = 0; i < dim*dim; i++)
    {
        x[i] = dist(gen);
        y[i] = dist(gen);
    }

    for (size_t row = 0; row < dim; row++)
        for (size_t col = 0; col < dim; col++)
        {
            float acc = 0;

            for (size_t k = 0; k < dim; k++)
                acc += x[row*dim + k] * y[k*dim + col];

            z[row*dim + col] = acc;
        }

    float t = 0;

    for (size_t i = 0; i < dim*dim; i++)
        t += z[i];

    cout << t << endl;

    delete x;
    delete y;
    delete z;
}

Compile and run:

$ g++ -std=gnu++11 -O3 test.cpp
$ time ./a.out

Here is the Octave/matlab implementation:

X = stdnormal_rnd(4096, 4096);
Y = stdnormal_rnd(4096, 4096);
Z = X*Y;
sum(sum(Z))

Run:

$ time octave < test.octave

Octave under the hood is using BLAS (I assume the sgemm function)

The hardware is i7 3930X on Linux x86-64 with 24 GB of ram. BLAS appears to be using two cores. Perhaps a hyperthreaded pair?

I found that the C++ version compiled with GCC 4.7 on -O3 took 9 minutes to execute:

real    9m2.126s
user    9m0.302s
sys         0m0.052s

The octave version took 6 seconds:

real    0m5.985s
user    0m10.881s
sys         0m0.144s

I understand that BLAS is optimized to all hell, and the naïve algorithm is totally ignoring caches and so on, but seriously -- 90 times?

Can anyone explain this difference? What exactly is the architecture of the BLAS implementation? I see it is using Fortran, but what is happening at the CPU level? What algorithm is it using? How is it using the CPU caches? What x86-64 machine instructions does it call? (Is it using advanced CPU features like AVX?) Where does it get this extra speed from?

Which key optimizations to the C++ algorithm could get it on par with the BLAS version?

I ran octave under gdb and stopped it half way through computation a few times. It had started a second thread and here are the stacks (all stops it looked similar):

(gdb) thread 1
#0  0x00007ffff6e17148 in pthread_join () from /lib/x86_64-linux-gnu/libpthread.so.0
#1  0x00007ffff1626721 in ATL_join_tree () from /usr/lib/libblas.so.3
#2  0x00007ffff1626702 in ATL_join_tree () from /usr/lib/libblas.so.3
#3  0x00007ffff15ae357 in ATL_dptgemm () from /usr/lib/libblas.so.3
#4  0x00007ffff1384b59 in atl_f77wrap_dgemm_ () from /usr/lib/libblas.so.3
#5  0x00007ffff193effa in dgemm_ () from /usr/lib/libblas.so.3
#6  0x00007ffff6049727 in xgemm(Matrix const&, Matrix const&, blas_trans_type, blas_trans_type) () from /usr/lib/x86_64-linux-gnu/liboctave.so.1
#7  0x00007ffff6049954 in operator*(Matrix const&, Matrix const&) () from /usr/lib/x86_64-linux-gnu/liboctave.so.1
#8  0x00007ffff7839e4e in ?? () from /usr/lib/x86_64-linux-gnu/liboctinterp.so.1
#9  0x00007ffff765a93a in do_binary_op(octave_value::binary_op, octave_value const&, octave_value const&) () from /usr/lib/x86_64-linux-gnu/liboctinterp.so.1
#10 0x00007ffff76c4190 in tree_binary_expression::rvalue1(int) () from /usr/lib/x86_64-linux-gnu/liboctinterp.so.1
#11 0x00007ffff76c33a5 in tree_simple_assignment::rvalue1(int) () from /usr/lib/x86_64-linux-gnu/liboctinterp.so.1
#12 0x00007ffff76d0864 in tree_evaluator::visit_statement(tree_statement&) () from /usr/lib/x86_64-linux-gnu/liboctinterp.so.1
#13 0x00007ffff76cffae in tree_evaluator::visit_statement_list(tree_statement_list&) () from /usr/lib/x86_64-linux-gnu/liboctinterp.so.1
#14 0x00007ffff757f6d4 in main_loop() () from /usr/lib/x86_64-linux-gnu/liboctinterp.so.1
#15 0x00007ffff7527abf in octave_main () from /usr/lib/x86_64-linux-gnu/liboctinterp.so.1

(gdb) thread 2
#0  0x00007ffff14ba4df in ATL_dJIK56x56x56TN56x56x0_a1_b1 () from /usr/lib/libblas.so.3
(gdb) bt
#0  0x00007ffff14ba4df in ATL_dJIK56x56x56TN56x56x0_a1_b1 () from /usr/lib/libblas.so.3
#1  0x00007ffff15a5fd7 in ATL_dmmIJK2 () from /usr/lib/libblas.so.3
#2  0x00007ffff15a6ae4 in ATL_dmmIJK () from /usr/lib/libblas.so.3
#3  0x00007ffff1518e65 in ATL_dgemm () from /usr/lib/libblas.so.3
#4  0x00007ffff15adf7a in ATL_dptgemm0 () from /usr/lib/libblas.so.3
#5  0x00007ffff6e15e9a in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0
#6  0x00007ffff6b41cbd in clone () from /lib/x86_64-linux-gnu/libc.so.6
#7  0x0000000000000000 in ?? ()

It is calling BLAS gemm as expected.

The first thread appears to be joining the second, so I am not sure whether these two threads account for the 200% CPU usage observed or not.

Which library is ATL_dgemm libblas.so.3 and where is its code?

$ ls -al /usr/lib/libblas.so.3
/usr/lib/libblas.so.3 -> /etc/alternatives/libblas.so.3

$ ls -al /etc/alternatives/libblas.so.3
/etc/alternatives/libblas.so.3 -> /usr/lib/atlas-base/atlas/libblas.so.3

$ ls -al /usr/lib/atlas-base/atlas/libblas.so.3
/usr/lib/atlas-base/atlas/libblas.so.3 -> libblas.so.3.0

$ ls -al /usr/lib/atlas-base/atlas/libblas.so.3.0
/usr/lib/atlas-base/atlas/libblas.so.3.0

$ dpkg -S /usr/lib/atlas-base/atlas/libblas.so.3.0
libatlas3-base: /usr/lib/atlas-base/atlas/libblas.so.3.0

$ apt-get source libatlas3-base

It is ATLAS 3.8.4

Here are the optimizations I later implemented:

Using a tiled approach where I preload 64x64 blocks of X, Y and Z into separate arrays.

Changing the calculation of each block so that the inner loop looks like this:

for (size_t tcol = 0; tcol < block_width; tcol++)
    bufz[trow][tcol] += B * bufy[tk][tcol];

This allows GCC to autovectorize to SIMD instructions and also allows for instruction level parallelism (I think).

Turning on march=corei7-avx. This gains 30% extra speed but is cheating because I think the BLAS library is prebuilt.

Here is the code:

#include <iostream>
#include <algorithm>

using namespace std;

constexpr size_t dim = 4096;
constexpr size_t block_width = 64;
constexpr size_t num_blocks = dim / block_width;

double X[dim][dim], Y[dim][dim], Z[dim][dim];

double bufx[block_width][block_width];
double bufy[block_width][block_width];
double bufz[block_width][block_width];

void calc_block()
{
    for (size_t trow = 0; trow < block_width; trow++)
        for (size_t tk = 0; tk < block_width; tk++)
        {
            double B = bufx[trow][tk];

            for (size_t tcol = 0; tcol < block_width; tcol++)
                bufz[trow][tcol] += B * bufy[tk][tcol];
        }
}

int main()
{
    random_device rd;
    mt19937 gen(rd());
    normal_distribution<double> dist(0, 1);

    for (size_t row = 0; row < dim; row++)
        for (size_t col = 0; col < dim; col++)
        {
            X[row][col] = dist(gen);
            Y[row][col] = dist(gen);
            Z[row][col] = 0;
        }

    for (size_t block_row = 0; block_row < num_blocks; block_row++)
        for (size_t block_col = 0; block_col < num_blocks; block_col++)
        {
            for (size_t trow = 0; trow < block_width; trow++)
                for (size_t tcol = 0; tcol < block_width; tcol++)
                    bufz[trow][tcol] = 0;

            for (size_t block_k = 0; block_k < num_blocks; block_k++)
            {
                for (size_t trow = 0; trow < block_width; trow++)
                    for (size_t tcol = 0; tcol < block_width; tcol++)
                    {
                        bufx[trow][tcol] = X[block_row*block_width + trow][block_k*block_width + tcol];
                        bufy[trow][tcol] = Y[block_k*block_width + trow][block_col*block_width + tcol];
                    }

                calc_block();
            }

            for (size_t trow = 0; trow < block_width; trow++)
                for (size_t tcol = 0; tcol < block_width; tcol++)
                    Z[block_row*block_width + trow][block_col*block_width + tcol] = bufz[trow][tcol];

        }

    double t = 0;

    for (size_t row = 0; row < dim; row++)
        for (size_t col = 0; col < dim; col++)
            t += Z[row][col];

    cout << t << endl;
}

All the action is in the calc_block function - over 90% of the time is spent in it.

The new time is:

real    0m17.370s
user    0m17.213s
sys 0m0.092s

Which is much closer.

The decompile of the calc_block function is as follows:

0000000000401460 <_Z10calc_blockv>:
  401460:   b8 e0 21 60 00          mov    $0x6021e0,%eax
  401465:   41 b8 e0 23 61 00       mov    $0x6123e0,%r8d
  40146b:   31 ff                   xor    %edi,%edi
  40146d:   49 29 c0                sub    %rax,%r8
  401470:   49 8d 34 00             lea    (%r8,%rax,1),%rsi
  401474:   48 89 f9                mov    %rdi,%rcx
  401477:   ba e0 a1 60 00          mov    $0x60a1e0,%edx
  40147c:   48 c1 e1 09             shl    $0x9,%rcx
  401480:   48 81 c1 e0 21 61 00    add    $0x6121e0,%rcx
  401487:   66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  40148e:   00 00 
  401490:   c4 e2 7d 19 01          vbroadcastsd (%rcx),%ymm0
  401495:   48 83 c1 08             add    $0x8,%rcx
  401499:   c5 fd 59 0a             vmulpd (%rdx),%ymm0,%ymm1
  40149d:   c5 f5 58 08             vaddpd (%rax),%ymm1,%ymm1
  4014a1:   c5 fd 29 08             vmovapd %ymm1,(%rax)
  4014a5:   c5 fd 59 4a 20          vmulpd 0x20(%rdx),%ymm0,%ymm1
  4014aa:   c5 f5 58 48 20          vaddpd 0x20(%rax),%ymm1,%ymm1
  4014af:   c5 fd 29 48 20          vmovapd %ymm1,0x20(%rax)
  4014b4:   c5 fd 59 4a 40          vmulpd 0x40(%rdx),%ymm0,%ymm1
  4014b9:   c5 f5 58 48 40          vaddpd 0x40(%rax),%ymm1,%ymm1
  4014be:   c5 fd 29 48 40          vmovapd %ymm1,0x40(%rax)
  4014c3:   c5 fd 59 4a 60          vmulpd 0x60(%rdx),%ymm0,%ymm1
  4014c8:   c5 f5 58 48 60          vaddpd 0x60(%rax),%ymm1,%ymm1
  4014cd:   c5 fd 29 48 60          vmovapd %ymm1,0x60(%rax)
  4014d2:   c5 fd 59 8a 80 00 00    vmulpd 0x80(%rdx),%ymm0,%ymm1
  4014d9:   00 
  4014da:   c5 f5 58 88 80 00 00    vaddpd 0x80(%rax),%ymm1,%ymm1
  4014e1:   00 
  4014e2:   c5 fd 29 88 80 00 00    vmovapd %ymm1,0x80(%rax)
  4014e9:   00 
  4014ea:   c5 fd 59 8a a0 00 00    vmulpd 0xa0(%rdx),%ymm0,%ymm1
  4014f1:   00 
  4014f2:   c5 f5 58 88 a0 00 00    vaddpd 0xa0(%rax),%ymm1,%ymm1
  4014f9:   00 
  4014fa:   c5 fd 29 88 a0 00 00    vmovapd %ymm1,0xa0(%rax)
  401501:   00 
  401502:   c5 fd 59 8a c0 00 00    vmulpd 0xc0(%rdx),%ymm0,%ymm1
  401509:   00 
  40150a:   c5 f5 58 88 c0 00 00    vaddpd 0xc0(%rax),%ymm1,%ymm1
  401511:   00 
  401512:   c5 fd 29 88 c0 00 00    vmovapd %ymm1,0xc0(%rax)
  401519:   00 
  40151a:   c5 fd 59 8a e0 00 00    vmulpd 0xe0(%rdx),%ymm0,%ymm1
  401521:   00 
  401522:   c5 f5 58 88 e0 00 00    vaddpd 0xe0(%rax),%ymm1,%ymm1
  401529:   00 
  40152a:   c5 fd 29 88 e0 00 00    vmovapd %ymm1,0xe0(%rax)
  401531:   00 
  401532:   c5 fd 59 8a 00 01 00    vmulpd 0x100(%rdx),%ymm0,%ymm1
  401539:   00 
  40153a:   c5 f5 58 88 00 01 00    vaddpd 0x100(%rax),%ymm1,%ymm1
  401541:   00 
  401542:   c5 fd 29 88 00 01 00    vmovapd %ymm1,0x100(%rax)
  401549:   00 
  40154a:   c5 fd 59 8a 20 01 00    vmulpd 0x120(%rdx),%ymm0,%ymm1
  401551:   00 
  401552:   c5 f5 58 88 20 01 00    vaddpd 0x120(%rax),%ymm1,%ymm1
  401559:   00 
  40155a:   c5 fd 29 88 20 01 00    vmovapd %ymm1,0x120(%rax)
  401561:   00 
  401562:   c5 fd 59 8a 40 01 00    vmulpd 0x140(%rdx),%ymm0,%ymm1
  401569:   00 
  40156a:   c5 f5 58 88 40 01 00    vaddpd 0x140(%rax),%ymm1,%ymm1
  401571:   00 
  401572:   c5 fd 29 88 40 01 00    vmovapd %ymm1,0x140(%rax)
  401579:   00 
  40157a:   c5 fd 59 8a 60 01 00    vmulpd 0x160(%rdx),%ymm0,%ymm1
  401581:   00 
  401582:   c5 f5 58 88 60 01 00    vaddpd 0x160(%rax),%ymm1,%ymm1
  401589:   00 
  40158a:   c5 fd 29 88 60 01 00    vmovapd %ymm1,0x160(%rax)
  401591:   00 
  401592:   c5 fd 59 8a 80 01 00    vmulpd 0x180(%rdx),%ymm0,%ymm1
  401599:   00 
  40159a:   c5 f5 58 88 80 01 00    vaddpd 0x180(%rax),%ymm1,%ymm1
  4015a1:   00 
  4015a2:   c5 fd 29 88 80 01 00    vmovapd %ymm1,0x180(%rax)
  4015a9:   00 
  4015aa:   c5 fd 59 8a a0 01 00    vmulpd 0x1a0(%rdx),%ymm0,%ymm1
  4015b1:   00 
  4015b2:   c5 f5 58 88 a0 01 00    vaddpd 0x1a0(%rax),%ymm1,%ymm1
  4015b9:   00 
  4015ba:   c5 fd 29 88 a0 01 00    vmovapd %ymm1,0x1a0(%rax)
  4015c1:   00 
  4015c2:   c5 fd 59 8a c0 01 00    vmulpd 0x1c0(%rdx),%ymm0,%ymm1
  4015c9:   00 
  4015ca:   c5 f5 58 88 c0 01 00    vaddpd 0x1c0(%rax),%ymm1,%ymm1
  4015d1:   00 
  4015d2:   c5 fd 29 88 c0 01 00    vmovapd %ymm1,0x1c0(%rax)
  4015d9:   00 
  4015da:   c5 fd 59 82 e0 01 00    vmulpd 0x1e0(%rdx),%ymm0,%ymm0
  4015e1:   00 
  4015e2:   c5 fd 58 80 e0 01 00    vaddpd 0x1e0(%rax),%ymm0,%ymm0
  4015e9:   00 
  4015ea:   48 81 c2 00 02 00 00    add    $0x200,%rdx
  4015f1:   48 39 ce                cmp    %rcx,%rsi
  4015f4:   c5 fd 29 80 e0 01 00    vmovapd %ymm0,0x1e0(%rax)
  4015fb:   00 
  4015fc:   0f 85 8e fe ff ff       jne    401490 <_Z10calc_blockv+0x30>
  401602:   48 83 c7 01             add    $0x1,%rdi
  401606:   48 05 00 02 00 00       add    $0x200,%rax
  40160c:   48 83 ff 40             cmp    $0x40,%rdi
  401610:   0f 85 5a fe ff ff       jne    401470 <_Z10calc_blockv+0x10>
  401616:   c5 f8 77                vzeroupper 
  401619:   c3                      retq   
  40161a:   66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
4

5 に答える 5

20

コードとBLASのパフォーマンスの違いの原因となる3つの要因を次に示します(さらに、Strassenのアルゴリズムに関する注意事項)。

内側のループで、にk、がありますy[k*dim + col]。メモリキャッシュの配置方法により、の連続する値はk同じで、同じキャッシュセットdimcolマップされます。キャッシュの構造では、各メモリアドレスに1つのキャッシュセットがあり、キャッシュ内にある間、その内容を保持する必要があります。各キャッシュセットには複数の行があり(通常は4行)、これらの各行には、その特定のキャッシュセットにマップされる任意のメモリアドレスを保持できます。

内部ループyはこのように反復するため、からの要素を使用するたびに、yその要素のメモリを前の反復と同じセットにロードする必要があります。これにより、セット内の以前のキャッシュラインの1つが強制的に削除されます。次に、colループの次の反復で、のすべての要素がyキャッシュから削除されたため、それらを再度再ロードする必要があります。

したがって、ループがの要素をロードするたびyに、メモリからロードする必要があり、これには多くのCPUサイクルが必要です。

高性能コードは、2つの方法でこれを回避します。1つは、作業を小さなブロックに分割することです。行と列は小さいサイズに分割され、キャッシュライン内のすべての要素を使用し、次のブロックに進む前に各要素を数回使用できる短いループで処理されます。したがって、の要素xおよびの要素への参照のほとんどはy、多くの場合、単一のプロセッササイクルでキャッシュから取得されます。2つ目は、状況によっては、コードがデータをマトリックスの列(ジオメトリが原因でキャッシュをスラッシングする)から一時バッファーの行(スラッシングを回避する)にコピーすることです。これにより、ほとんどのメモリ参照をメモリからではなくキャッシュから提供できるようになります。

もう1つの要因は、単一命令複数データ(SIMD)機能の使用です。最近の多くのプロセッサにはfloat、1つの命令に複数の要素(4つの要素が一般的ですが、現在は8つを実行するものもあります)をロードし、複数の要素を格納し、複数の要素を追加する(たとえば、これら4つの要素のそれぞれについて、それらの4つのうちの対応する1つに追加する)命令があります。 )、複数の要素を乗算するなど。これらの命令を使用するように作業を調整できる場合は、そのような命令を使用するだけで、コードがすぐに4倍高速になります。

これらの命令は、標準Cでは直接アクセスできません。一部のオプティマイザは、可能な場合にそのような命令を使用しようとしますが、この最適化は困難であり、それから多くの利益を得るのは一般的ではありません。多くのコンパイラは、これらの命令へのアクセスを提供する言語の拡張機能を提供します。個人的には、SIMDを使用するためにアセンブリ言語で書くことを好みます。

もう1つの要因は、プロセッサで命令レベルの並列実行機能を使用することです。内側のループで、accが更新されていることを確認してください。acc次のイテレーションは、前のイテレーションの更新が完了するまで追加できませんacc。代わりに、高性能コードは複数の合計を並行して実行し続けます(複数のSIMD合計でも)。この結果、ある合計の加算が実行されている間に、別の合計の加算が開始されます。今日のプロセッサでは、一度に4つ以上の浮動小数点演算をサポートするのが一般的です。書かれているように、あなたのコードはこれをまったく行うことができません。一部のコンパイラは、ループを再配置することによってコードを最適化しようとしますが、これには、特定のループの反復が互いに独立しているか、別のループなどと交換できることをコンパイラが確認できる必要があります。

キャッシュを効果的に使用するとパフォーマンスが10倍向上し、SIMDはさらに4つ、命令レベルの並列性はさらに4つ、合計で160を提供することは非常に現実的です。

これは、このウィキペディアのページに基づいた、シュトラッセンのアルゴリズムの効果の非常に大まかな見積もりです。ウィキペディアのページには、Strassenはn = 100付近の直接乗算よりもわずかに優れていると記載されています。これは、実行時間の定数係数の比率が100 3 / 1002.807≈2.4であることを示しています。明らかに、これはプロセッサモデル、キャッシュ効果と相互作用するマトリックスサイズなどによって大きく異なります。ただし、単純な外挿では、Strassenはn = 4096((4096/100) 3-2.807≈2.05)での直接乗算の約2倍優れていることが示されています。繰り返しになりますが、これは単なる見積もりです。

後の最適化については、内側のループで次のコードを検討してください。

bufz[trow][tcol] += B * bufy[tk][tcol];

これに関する潜在的な問題の1つはbufz、一般に、重複する可能性があることbufyです。bufzとのグローバル定義を使用しbufyているため、この場合、コンパイラはそれらが重複していないことを認識している可能性があります。bufzただし、このコードをパラメーターとして渡されるサブルーチンに移動する場合bufy、特にそのサブルーチンを別のソースファイルでコンパイルする場合、コンパイラーはそれを認識し、重複しない可能性が低くなりbufzますbufy。その場合、この反復のは別の反復bufz[trow][tcol]と同じである可能性があるため、コンパイラはコードをベクトル化またはその他の方法で並べ替えることはできません。bufy[tk][tcol]

コンパイラがサブルーチンが別の現在のソースモジュールで呼び出されていることを確認できたとしてもbufzbufyルーチンにexternリンケージ(デフォルト)がある場合、コンパイラはルーチンが外部モジュールから呼び出されることを許可する必要があるため、重複している場合bufzに正しく機能するコードを生成します。bufy(コンパイラーがこれに対処する1つの方法は、ルーチンの2つのバージョンを生成することです。1つは外部モジュールから呼び出され、もう1つは現在コンパイルされているモジュールから呼び出されます。それが行われるかどうかは、コンパイラー、最適化スイッチによって異なります。など。)ルーチンを次のように宣言した場合staticの場合、コンパイラは、外部モジュールから呼び出すことができないことを認識します(アドレスを取得し、アドレスが現在のモジュールの外部に渡される可能性がある場合を除く)。

もう1つの潜在的な問題は、コンパイラがこのコードをベクトル化しても、実行するプロセッサに最適なコードを生成するとは限らないことです。生成されたアセンブリコードを見ると、コンパイラが%ymm1繰り返し使用しているように見えます。何度も何度も、メモリからの値を乗算し、%ymm1メモリからに値を追加し、からメモリに値を%ymm1格納%ymm1します。これには2つの問題があります。

1つは、これらの部分的な合計が頻繁にメモリに保存されることを望まないことです。レジスタに多くの加算を蓄積する必要があり、レジスタがメモリに書き込まれるのはまれです。コンパイラにこれを行うように説得するには、一時オブジェクトに部分和を保持し、ループの完了後にそれらをメモリに書き込むことについて明示的にコードを書き直す必要があります。

2つ目は、これらの命令は名目上シリアルに依存しています。乗算が完了するまで追加を開始できず、ストアは追加が完了するまでメモリに書き込むことができません。Core i7には、アウトオブオーダー実行のための優れた機能があります。したがって、実行を開始するために待機しているaddがありますが、命令ストリームの後半で乗算を調べて開始します。(その乗算も使用しますが%ymm1、プロセッサはその場でレジスタを再マップするため、別の内部レジスタを使用してこの乗算を実行します。)コードが連続した依存関係でいっぱいであっても、プロセッサは一度に複数の命令を実行しようとします。ただし、多くのことがこれを妨げる可能性があります。プロセッサが名前の変更に使用する内部レジスタが不足する可能性があります。使用するメモリアドレスは、誤った競合に遭遇する可能性があります。(プロセッサは、メモリアドレスの下位ビットを12個ほど調べて、そのアドレスが以前の命令からロードまたは保存しようとしている別のアドレスと同じであるかどうかを確認します。ビットが等しい場合、プロセッサは次のようになります。アドレス全体が異なることを確認できるまで、現在のロードまたはストアを遅延させるため。この遅延は、現在のロードまたはストアだけでなく、それ以上の遅延を引き起こす可能性があります。)したがって、

それが、私がアセンブリで高性能コードを書くことを好むもう1つの理由です。Cでそれを行うには、独自のSIMDコードの一部を記述し(言語拡張機能を使用)、ループを手動で展開する(複数の反復を書き出す)などして、コンパイラーにこのような命令を与えるように説得する必要があります。

バッファにコピーしたり、バッファからコピーしたりする場合、同様の問題が発生する可能性があります。しかし、あなたは時間の90%がに費やされていると報告しcalc_blockているので、私はこれを詳しく調べていません。

于 2013-01-26T00:27:39.027 に答える
6

Strassenのアルゴリズムには、ナイーブアルゴリズムに比べて2つの利点があります。

  1. 他の回答が正しく指摘しているように、操作の数の点で時間計算量が向上しています。
  2. これは、キャッシュを無視するアルゴリズムです。キャッシュミスの数の違いは、のオーダーですB*M½。ここで、Bはキャッシュラインサイズ、Mはキャッシュサイズです。

2番目のポイントはあなたが経験している減速の多くを説明していると思います。Linuxでアプリケーションを実行している場合は、ツールを使用してアプリケーションを実行することをお勧めします。このツールを使用するとperf、プログラムで発生しているキャッシュミスの数がわかります。

于 2013-01-26T00:35:19.000 に答える
2

情報の信頼性はわかりませんが、ウィキペディアによると、BLAS は大きな行列に Strassen のアルゴリズムを使用しています。そして、あなたは確かに大きいです。これは O(n^2.807) 前後であり、O(n^3) ナイーブ アルゴリズムよりも優れています。

于 2013-01-25T23:59:54.260 に答える
1

これは非常に複雑なトピックであり、上記の投稿で Eric が適切に回答しています。私はちょうどこの方向で有用な参照を指摘したい, ページ 84:

http://www.rrze.fau.de/dienste/arbeiten-rechnen/hpc/HPC4SE/

これは、ブロッキングの上に「ループのアンロールとジャム」を行うことを提案しています。

誰でもこの違いを説明できますか?

一般的な説明は、操作数/データ数の比が O(N^3)/O(N^2) であるということです。したがって、行列と行列の乗算はキャッシュにバインドされたアルゴリズムです。つまり、行列のサイズが大きい場合でも、一般的なメモリ帯域幅のボトルネックに悩まされることはありません。コードが適切に最適化されていれば、CPU の最大 90% のピーク パフォーマンスを得ることができます。エリックによって詳しく説明された最適化の可能性は、あなたが観察したように途方もないものです。実際、最高のパフォーマンスを発揮するコードを確認し、最終的なプログラムを別のコンパイラでコンパイルするのは非常に興味深いことです (インテルは通常、最高のコンパイラを自慢しています)。

于 2013-01-28T07:18:03.200 に答える
-1

違いの約半分は、アルゴリズムの改善で説明されています。(4096 * 4096)^ 3はアルゴリズムの複雑さ、つまり4.7x10 ^ 21であり、(4096 * 4096)^2.807は1x10^20です。これは約47倍の違いです。

他の2xは、キャッシュ、SSE命令、およびその他のそのような低レベルの最適化のよりインテリジェントな使用によって説明されます。

編集:私は嘘をつきます、nは幅であり、幅^2ではありません。アルゴリズムは実際には約4倍しか占めないので、まだ約22倍残っています。スレッド、キャッシュ、およびSSE命令は、そのようなことを十分に説明している可能性があります。

于 2013-01-26T00:22:41.327 に答える