Cortex-A72で-O3ではなく-O0を使用した単純なタイトループのサイクルでこの高い変動が発生する原因は何ですか?


9

コードの一部に対して非常に一貫性のあるランタイムを取得するためにいくつかの実験を行っています。私が現在計時しているコードは、かなり恣意的なCPUバウンドのワークロードです。

int cpu_workload_external_O3(){
    int x = 0;
    for(int ind = 0; ind < 12349560; ind++){
        x = ((x ^ 0x123) + x * 3) % 123456;
    }
    return x;
}

割り込みを無効にし、上記の関数の10回の試行を実行するカーネルモジュールを作成しました。各試行のタイミングは、前後のクロックサイクルカウンターの差をとることによって計っています。その他の注意事項:

  • マシンはARM Cortex-A72であり、それぞれ4コアの4ソケット(それぞれに独自のL1キャッシュがある)
  • クロック周波数スケーリングはオフです
  • ハイパースレッディングはサポートされていません
  • マシンは、一部の最低限のシステムプロセスを除いて、実質的に何も実行していません

言い換えると、システム変動のほとんど/すべての原因が説明されていると私は信じています。特に、割り込みを無効にしてカーネルモジュールとして実行した場合spin_lock_irqsave()、コードは実行間でほぼ同じパフォーマンスを達成するはずです(たぶん小さなパフォーマンスヒット)最初の実行では、いくつかの命令が最初にキャッシュにプルされますが、それだけです)。

実際、ベンチマークされたコードがでコンパイルされた場合、-O3平均で〜135,845,192のうち最大で200サイクルの範囲があり、ほとんどの試行でまったく同じ時間がかかりました。ただし、を使用してコンパイルする-O0と、範囲は262,710,916のうち158,386サイクルまで増加します。範囲とは、最長実行時間と最短実行時間の差を意味します。さらに、-O0コードでは、どの試行が最も遅い/最も速いかについて一貫性があまりありません-直感的には、ある場合には、最も速いものが最初であり、最も遅いものが直後のものでした!

それで-O0コードの変動性のこの高い上限を引き起こしている可能性があるものは何ですか?アセンブリを見ると、-O3コードはすべて(?)をレジスタに格納しているようですが、-O0コードにはたくさんの参照spがあるため、メモリにアクセスしているようです。しかし、それでも、すべてがL1キャッシュに取り込まれ、かなり確定的なアクセス時間でそこに座っていることが期待されます。


コード

ベンチマーク対象のコードは上記のスニペットにあります。組み立ては下にあります。とgcc 7.4.0以外はフラグなしでコンパイルされました。-O0-O3

-O0

0000000000000000 <cpu_workload_external_O0>:
   0:   d10043ff        sub     sp, sp, #0x10
   4:   b9000bff        str     wzr, [sp, #8]
   8:   b9000fff        str     wzr, [sp, #12]
   c:   14000018        b       6c <cpu_workload_external_O0+0x6c>
  10:   b9400be1        ldr     w1, [sp, #8]
  14:   52802460        mov     w0, #0x123                      // #291
  18:   4a000022        eor     w2, w1, w0
  1c:   b9400be1        ldr     w1, [sp, #8]
  20:   2a0103e0        mov     w0, w1
  24:   531f7800        lsl     w0, w0, #1
  28:   0b010000        add     w0, w0, w1
  2c:   0b000040        add     w0, w2, w0
  30:   528aea61        mov     w1, #0x5753                     // #22355
  34:   72a10fc1        movk    w1, #0x87e, lsl #16
  38:   9b217c01        smull   x1, w0, w1
  3c:   d360fc21        lsr     x1, x1, #32
  40:   130c7c22        asr     w2, w1, #12
  44:   131f7c01        asr     w1, w0, #31
  48:   4b010042        sub     w2, w2, w1
  4c:   529c4801        mov     w1, #0xe240                     // #57920
  50:   72a00021        movk    w1, #0x1, lsl #16
  54:   1b017c41        mul     w1, w2, w1
  58:   4b010000        sub     w0, w0, w1
  5c:   b9000be0        str     w0, [sp, #8]
  60:   b9400fe0        ldr     w0, [sp, #12]
  64:   11000400        add     w0, w0, #0x1
  68:   b9000fe0        str     w0, [sp, #12]
  6c:   b9400fe1        ldr     w1, [sp, #12]
  70:   528e0ee0        mov     w0, #0x7077                     // #28791
  74:   72a01780        movk    w0, #0xbc, lsl #16
  78:   6b00003f        cmp     w1, w0
  7c:   54fffcad        b.le    10 <cpu_workload_external_O0+0x10>
  80:   b9400be0        ldr     w0, [sp, #8]
  84:   910043ff        add     sp, sp, #0x10
  88:   d65f03c0        ret

-O3

0000000000000000 <cpu_workload_external_O3>:
   0:   528e0f02        mov     w2, #0x7078                     // #28792
   4:   5292baa4        mov     w4, #0x95d5                     // #38357
   8:   529c4803        mov     w3, #0xe240                     // #57920
   c:   72a01782        movk    w2, #0xbc, lsl #16
  10:   52800000        mov     w0, #0x0                        // #0
  14:   52802465        mov     w5, #0x123                      // #291
  18:   72a043e4        movk    w4, #0x21f, lsl #16
  1c:   72a00023        movk    w3, #0x1, lsl #16
  20:   4a050001        eor     w1, w0, w5
  24:   0b000400        add     w0, w0, w0, lsl #1
  28:   0b000021        add     w1, w1, w0
  2c:   71000442        subs    w2, w2, #0x1
  30:   53067c20        lsr     w0, w1, #6
  34:   9ba47c00        umull   x0, w0, w4
  38:   d364fc00        lsr     x0, x0, #36
  3c:   1b038400        msub    w0, w0, w3, w1
  40:   54ffff01        b.ne    20 <cpu_workload_external_O3+0x20>  // b.any
  44:   d65f03c0        ret

カーネルモジュール

トライアルを実行するコードは以下のとおりです。PMCCNTR_EL0各反復の前/後に読み取り、配列に差異を格納し、すべての試行にわたって最後に最小/最大時間を出力します。関数cpu_workload_external_O0cpu_workload_external_O3は、個別にコンパイルされてからリンクされる外部オブジェクトファイルにあります。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

#include "cpu.h"

static DEFINE_SPINLOCK(lock);

void runBenchmark(int (*benchmarkFunc)(void)){
    // Enable perf counters.
    u32 pmcr;
    asm volatile("mrs %0, pmcr_el0" : "=r" (pmcr));
    asm volatile("msr pmcr_el0, %0" : : "r" (pmcr|(1)));

    // Run trials, storing the time of each in `clockDiffs`.
    u32 result = 0;
    #define numtrials 10
    u32 clockDiffs[numtrials] = {0};
    u32 clockStart, clockEnd;
    for(int trial = 0; trial < numtrials; trial++){
        asm volatile("isb; mrs %0, PMCCNTR_EL0" : "=r" (clockStart));
        result += benchmarkFunc();
        asm volatile("isb; mrs %0, PMCCNTR_EL0" : "=r" (clockEnd));

        // Reset PMCCNTR_EL0.
        asm volatile("mrs %0, pmcr_el0" : "=r" (pmcr));
        asm volatile("msr pmcr_el0, %0" : : "r" (pmcr|(((uint32_t)1) << 2)));

        clockDiffs[trial] = clockEnd - clockStart;
    }

    // Compute the min and max times across all trials.
    u32 minTime = clockDiffs[0];
    u32 maxTime = clockDiffs[0];
    for(int ind = 1; ind < numtrials; ind++){
        u32 time = clockDiffs[ind];
        if(time < minTime){
            minTime = time;
        } else if(time > maxTime){
            maxTime = time;
        }
    }

    // Print the result so the benchmark function doesn't get optimized out.
    printk("result: %d\n", result);

    printk("diff: max %d - min %d = %d cycles\n", maxTime, minTime, maxTime - minTime);
}

int init_module(void) {
    printk("enter\n");
    unsigned long flags;
    spin_lock_irqsave(&lock, flags);

    printk("-O0\n");
    runBenchmark(cpu_workload_external_O0);

    printk("-O3\n");
    runBenchmark(cpu_workload_external_O3);

    spin_unlock_irqrestore(&lock, flags);
    return 0;
}

void cleanup_module(void) {
    printk("exit\n");
}

ハードウェア

$ lscpu
Architecture:        aarch64
Byte Order:          Little Endian
CPU(s):              16
On-line CPU(s) list: 0-15
Thread(s) per core:  1
Core(s) per socket:  4
Socket(s):           4
NUMA node(s):        1
Vendor ID:           ARM
Model:               3
Model name:          Cortex-A72
Stepping:            r0p3
BogoMIPS:            166.66
L1d cache:           32K
L1i cache:           48K
L2 cache:            2048K
NUMA node0 CPU(s):   0-15
Flags:               fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
$ lscpu --extended
CPU NODE SOCKET CORE L1d:L1i:L2 ONLINE
0   0    0      0    0:0:0      yes
1   0    0      1    1:1:0      yes
2   0    0      2    2:2:0      yes
3   0    0      3    3:3:0      yes
4   0    1      4    4:4:1      yes
5   0    1      5    5:5:1      yes
6   0    1      6    6:6:1      yes
7   0    1      7    7:7:1      yes
8   0    2      8    8:8:2      yes
9   0    2      9    9:9:2      yes
10  0    2      10   10:10:2    yes
11  0    2      11   11:11:2    yes
12  0    3      12   12:12:3    yes
13  0    3      13   13:13:3    yes
14  0    3      14   14:14:3    yes
15  0    3      15   15:15:3    yes
$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
node 0 size: 32159 MB
node 0 free: 30661 MB
node distances:
node   0
  0:  10

サンプル測定

以下は、カーネルモジュールの1回の実行からの出力です。

[902574.112692] kernel-module: running on cpu 15                                                                                                                                      
[902576.403537] kernel-module: trial 00: 309983568 74097394 98796602 <-- max
[902576.403539] kernel-module: trial 01: 309983562 74097397 98796597                                                                                                                  
[902576.403540] kernel-module: trial 02: 309983562 74097397 98796597                                                                                                                  
[902576.403541] kernel-module: trial 03: 309983562 74097397 98796597
[902576.403543] kernel-module: trial 04: 309983562 74097397 98796597
[902576.403544] kernel-module: trial 05: 309983562 74097397 98796597                                                                                                                  
[902576.403545] kernel-module: trial 06: 309983562 74097397 98796597
[902576.403547] kernel-module: trial 07: 309983562 74097397 98796597
[902576.403548] kernel-module: trial 08: 309983562 74097397 98796597
[902576.403550] kernel-module: trial 09: 309983562 74097397 98796597                                                                                                                  
[902576.403551] kernel-module: trial 10: 309983562 74097397 98796597
[902576.403552] kernel-module: trial 11: 309983562 74097397 98796597
[902576.403554] kernel-module: trial 12: 309983562 74097397 98796597                                                                                                                  
[902576.403555] kernel-module: trial 13: 309849076 74097403 98796630 <-- min
[902576.403557] kernel-module: trial 14: 309983562 74097397 98796597                                                                                                                  
[902576.403558] kernel-module: min time: 309849076
[902576.403559] kernel-module: max time: 309983568                                                                                                                                    
[902576.403560] kernel-module: diff: 134492

各試行について、報告される値は、サイクル数(0x11)、L1Dアクセス数(0x04)、L1Iアクセス数(0x14)です。このARM PMUリファレンスのセクション11.8を使用しています)。


2
実行中の他のスレッドはありますか?バス帯域幅とキャッシュスペースの競合を引き起こすメモリアクセスが影響している可能性があります。
PRL

になり得る。私はコアをisolcpuしていません。それでも、カーネルスレッドがソケット上の他のコアの1つでスケジュールされる可能性があります。しかし、私がlscpu --extended正しく理解している場合、各コアには独自のL1データキャッシュと命令キャッシュがあり、各ソケットには4つのコア用の共有L2キャッシュがあります。そのバスを多く「所有」している(完了まで、それはそのコアで実行されている唯一のものであるため)。ただし、このレベルのハードウェアについてはあまり知りません。
sevko

1
ええ、それは明らかに4ソケットとして報告されていますが、それは、16コアSoC内で相互接続がどのように配線されているかということの問題かもしれません。しかし、あなたは物理的なマシンを持っていますよね?ブランドとモデル番号はありますか?ふたが外れた場合は、おそらく実際に4つの別個のソケットがあるかどうかも確認できます。モボのベンダー/モデル番号を除いて、なぜこれが問題になるのかはわかりません。ベンチマークは純粋にシングルコアであり、キャッシュ内でホットのままである必要があるため、問題になるのはA72コア自体とそのストアバッファー+ストア転送だけです。
Peter Cordes

1
カーネルモジュールを変更して3つのカウンターを追跡し、サンプル出力をいくつか追加しました。興味深いのは、ほとんどの実行が一貫していることですが、ランダムな実行はかなり高速になります。この場合、最速の方が実際には非常にわずかに多くの L1アクセスを持っているように見えます。また、残念ながら私はマシンにアクセスできません。それはAWS a1.metalインスタンスです(物理ハードウェアの完全な所有権を与えるため、ハイパーバイザーなどからの干渉は見られません)。
sevko

1
興味深いことに、私がカーネルモジュールでこのコードをを介してすべてのCPUで同時に実行するとon_each_cpu()、それぞれが100回の試行にわたってほとんど変化しないことを報告します。
sevko

回答:


4

最近のLinuxカーネルでは、自動NUMAページ移行メカニズムがTLBエントリを定期的に削除して、NUMAの局所性を監視できるようにしています。TLBのリロードは、データがL1Dキャッシュに残っている場合でも、O0コードの速度を低下させます。

カーネルページでは、ページ移行メカニズムをアクティブにしないでください。

自動NUMAページ移行が有効になっているかどうかを確認します

$ cat /proc/sys/kernel/numa_balancing

あなたはそれを無効にすることができます

$ echo 0 > /proc/sys/kernel/numa_balancing

最近、いくつかの関連するテストを行っています。私は、L1キャッシュに快適に収まるメモリのバッファに大量のランダムアクセスを行うワークロードを実行しています。私は一連の試行を連続して実行し、実行時間は非常に一貫しています(文字通り0.001%未満です)。ただし、定期的に上向きの小さなスパイクがある場合を除きます。そのスパイクでは、ベンチマークは0.014%長く実行されます。これは小さいですが、これらのスパイクはそれぞれまったく同じ大きさで、スパイクは2秒ごとにほぼ正確に1回発生します。このマシンはnuma_balancing無効になっています。おそらくあなたはアイデアを持っていますか?
sevko

理解した。私は一日中パフォーマンスカウンターを見つめていましたが、根本的な原因はまったく無関係であることがわかりました。私はこれらのテストを静かなマシンのtmuxセッションで実行していました。2秒の間隔は、私のtmuxステータスラインの更新間隔と正確に一致しました。これにより、ネットワーク要求などが発生します。無効にすると、スパイクが消えます。スクリプトは唯一のL1データに触れ、異なるコアクラスタが分離されたコアクラスタ上で実行中のプロセスに影響を与えるた上で、私のステータスラインで実行する方法は考えていない...
sevko

2

分散は6 * 10 ^ -4のオーダーです。驚くほど1.3 * 10 ^ -6以上ですが、プログラムがキャッシュと通信すると、多くの同期操作に関与します。同期は常に無駄な時間を意味します。

興味深いのは、-O0、-O3の比較が、L1キャッシュヒットがレジスター参照の約2倍であるという一般的なルールをどのように模倣するかです。平均O3は、O0が実行する時間の51.70%で実行されます。下限/上限の分散を適用すると、(O3-200)/(O0 + 158386)となり、51.67%の改善が見られます。

要するに、はい、キャッシュは確定的ではありません。そして、あなたが見る低い変動は、より遅いデバイスとの同期から期待されるはずのものと一致しています。より確定的なレジスタのみのマシンと比較すると、それは大きな差異です。


命令はL1iキャッシュからフェッチされます。同じコアまたは他のコアのデータキャッシュと一貫性がないため、予期しないスローダウンの影響を受けないと言っているのではないでしょうか。しかし、いずれにせよ、Dr。Bandwidthの答えが正しい場合、差異はキャッシュ自体によるものではなく、カーネルによる定期的なdTLBの無効化によるものです。その説明はすべての観察結果を完全に説明します。ユーザー空間にロード/ストアを含めることによる分散の増加、およびカーネルモジュール内のループのタイミングを調整するときにこの低下が発生しないという事実。(Linuxカーネルメモリはスワップ可能ではありません。)
Peter Cordes

キャッシュは通常、ホットデータにアクセスしているときに確定的です。コア自体からのロード/ストアを妨害することなくコヒーレンシトラフィックを許可するために、マルチポート化できます。外乱が他のコアによるものであるというあなたの推測はもっともらしいですが、私はnuma_balancingTLBの無効化だけでおそらくそれを説明しています。
Peter Cordes

スヌーピングキャッシュには、要求を停止する必要がある割り込み不可能なシーケンスが必要です。1対2サイクル動作で10 ^ -4のスローダウンは、10 ^ 5動作ごとに1クロックのヒッコクを意味します。質問全体は実際にはノーオペレーションであり、差異はごくわずかです。
mevets
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.