Bir Cortex-A72'de -O0 değil -O3 değil, basit bir sıkı döngü için bu yüksek değişkenlik döngülerine ne sebep olur?


9

Bir kod parçası için son derece tutarlı çalışma zamanları almak etrafında bazı deneyler yapıyorum. Şu anda zamanlama kodu oldukça keyfi bir CPU bağlı iş yükü:

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

Kesmeleri devre dışı bırakan ve yukarıdaki fonksiyonun 10 denemesini çalıştıran bir çekirdek modülü yazdım, her bir denemeyi zaman döngüsü sayacındaki farkı önceki ve sonraki zamana göre alarak zamanladım. Dikkat edilmesi gereken diğer şeyler:

  • Makine, her biri 4 çekirdekli 4 soketli (her biri kendi L1 önbelleğine sahip) bir ARM Cortex-A72'dir
  • saat frekansı ölçeklendirme kapalı
  • hiper iş parçacığı desteklenmez
  • Makine bazı çıplak kemik sistemi işlemleri dışında neredeyse hiçbir şey çalıştırmıyor

Başka bir deyişle, sistem değişkenliği kaynaklarının çoğunun / tümünün hesaba katıldığına inanıyorum ve özellikle kesintileri devre dışı bırakılmış bir çekirdek modülü olarak çalıştırıldığında spin_lock_irqsave(), kodun hemen hemen aynı performansa ulaşması gerekir (belki küçük bir performans isabeti) ilk çalıştırmada bazı talimatlar önbelleğe alındığında, ancak bu kadar).

Gerçekten de, karşılaştırmalı kod derlendiğinde -O3, ortalama olarak 135.845.192'den en fazla 200 döngü gördüm, denemelerin çoğu tam olarak aynı süreyi aldı. Bununla birlikte , derlendiğinde -O0, aralık ~ 262.710.916'dan 158.386 devire kadar yükseldi. Aralığa göre, en uzun ve en kısa çalışma süreleri arasındaki farkı kastediyorum. Dahası, -O0kod için, denemelerin hangisinin en yavaş / en hızlı olduğu konusunda çok fazla tutarlılık yoktur - mantıksız olarak, bir kerede en hızlı ilk ve en yavaş olanıdır!

Peki , -O0koddaki değişkenlikte bu üst sınırın nedeni ne olabilir ? Derlemeye bakıldığında, -O3kodun her şeyi (?) Bir kayıt defterinde sakladığı görülürken, -O0kodun bir grup referansı vardır spve bu nedenle belleğe erişiyor gibi görünüyor. Ama o zaman bile, her şeyin L1 önbelleğine getirilmesini ve orada oldukça belirleyici bir erişim süresiyle oturmasını beklerdim.


kod

Kıyaslanan kod yukarıdaki kod parçasındadır. Montaj aşağıda. Her ikisi de ve gcc 7.4.0dışında hiçbir bayrak olmadan derlendi .-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

çekirdek modülü

Denemeleri çalıştıran kod aşağıdadır. PMCCNTR_EL0Her yinelemeden önce / sonra okur , farklılıkları bir dizide saklar ve tüm denemelerde sondaki min / maks sürelerini yazdırır. İşlevler cpu_workload_external_O0ve cpu_workload_external_O3ayrı olarak derlenen ve daha sonra bağlanan harici nesne dosyalarındadır.

#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");
}

Donanım

$ 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

Örnek Ölçümler

Çekirdek modülünün bir uygulamasından bazı çıktılar aşağıdadır:

[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

Her deneme için bildirilen değerler şunlardır: döngü sayısı (0x11), L1D erişim sayısı (0x04), L1I erişim sayısı (0x14). Bu ARM PMU referansının 11.8 bölümünü kullanıyorum ).


2
Çalışan başka iş parçacıkları var mı? Bellek bant genişliği ve önbellek alanı için rekabete neden olan bellek erişimleri etkili olabilir.
prl

Olabilirdi. Ben herhangi bir çekirdek isolcpu'd değil, ve o zaman bile bir çekirdek iplik soket üzerindeki diğer çekirdeklerden birinde zamanlanmış olabilir. Ancak lscpu --extendeddoğru anlıyorsam , her çekirdeğin kendi L1 veri ve talimat önbellekleri vardır ve daha sonra her soketin 4 çekirdeği için paylaşılan bir L2 önbelleği vardır, böylece L1 önbellek içinde her şey yapıldıysa kodun güzel olmasını beklerim çok "kendi" otobüs (tamamlanana kadar, çekirdek üzerinde çalışan tek şey olduğu gibi). Yine de bu düzeydeki donanım hakkında fazla bir şey bilmiyorum.
sevko

1
Evet, açıkça 4 soket olarak bildirildi, ancak bu, ara bağlantının 16 çekirdekli bir SoC içine nasıl bağlandığı meselesi olabilir. Ama fiziksel makineniz var, değil mi? Bunun için bir marka ve model numaranız var mı? Kapak çıkarsa, muhtemelen gerçekten 4 ayrı soketin olup olmadığını da doğrulayabilirsiniz. Belki de mobo'nun satıcı / model numarası haricinde, bunun neden önemli olduğunu anlamıyorum. Karşılaştırmanız tamamen tek çekirdektir ve önbellekte sıcak kalmalıdır, bu yüzden önemli olan tek şey A72 çekirdeğinin kendisi ve mağaza tamponu + mağaza iletmesidir.
Peter Cordes

1
Üç sayacı izlemek için çekirdek modülünü değiştirdim ve bazı örnek çıktılar ekledim. İlginç olan, çalışmaların çoğunun tutarlı olması, ancak daha sonra rastgele bir işlemin daha hızlı olması. Bu durumda, en hızlı olanın aslında biraz daha fazla L1 erişimine sahip olduğu anlaşılıyor , bu da bir yerlerde daha agresif bir şube tahmini anlamına geliyor. Ayrıca maalesef makineye erişimim yok. Bu bir AWS a1.metal örneğidir (fiziksel donanımın size tam olarak sahip olmasını sağlar, bu nedenle görünüşe göre bir hipervizörden parazit yoktur).
sevko

1
İlginçtir ki, çekirdek modülünü bu kodu tüm CPU'larda aynı anda çalıştırırsam on_each_cpu(), her biri 100 denemede neredeyse hiç değişkenlik bildirmez.
sevko

Yanıtlar:


4

Son Linux çekirdeğinde, otomatik NUMA sayfa taşıma mekanizması, TLA girişlerini periyodik olarak aşağı çekerek NUMA yerini izleyebiliyor. TLB yeniden yüklemeleri, veriler L1DCache'de kalsa bile O0 kodunu yavaşlatır.

Sayfa taşıma mekanizması, çekirdek sayfalarda etkinleştirilmemelidir.

Otomatik NUMA sayfası taşımanın etkin olup olmadığını kontrol edersiniz.

$ cat /proc/sys/kernel/numa_balancing

ve ile devre dışı bırakabilirsiniz

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

Son zamanlarda ilgili testler yapıyorum. L1 önbelleğine rahatça uyan bir bellek arabelleğine rasgele erişim sağlayan bir iş yükü çalıştırıyorum. Bir sürü arka arkaya deneme çalıştırıyorum ve çalışma süresi oldukça tutarlı ((kelimenin tam anlamıyla% 0.001'den daha az değişiyor), ancak periyodik olarak yukarı doğru küçük bir artış var. Bu artışta kıyaslama sadece% 0,014 daha uzun sürüyor. Bu küçüktür, ancak bu sivri uçların her biri tam olarak aynı büyüklüğe sahiptir ve sivri uç neredeyse her 2 saniyede bir kez meydana gelir. Bu makine numa_balancingdevre dışı. Belki bir fikrin var mı?
sevko

Anladım. Bütün gün mükemmel sayaçlara bakıyordum ama kök neden tamamen alakasız bir şeydi .. Bu testleri sessiz bir makinede bir tmux oturumunda yürütüyordum. 2 saniyelik aralık tam olarak tmux durum hattımın yenileme aralığına denk geldi, bu da diğer şeyler arasında bir ağ isteği yapıyor .. Devre dışı bırakılması ani artışların ortadan kalkmasına neden oldu. Farklı bir çekirdek kümede durum
satırım

2

Varyansınız 6 * 10 ^ -4 sırasıyla. Şok edici bir şekilde 1.3 * 10 ^ -6'dan fazla olsa da, programınız önbelleklerle konuştuğunda, birçok senkronize işlemde yer alır. Senkronize edilmiş her zaman zaman kaybı demektir.

İlginç bir şey, -O0, -O3 karşılaştırmanızın L1-önbellek isabetinin bir kayıt referansının yaklaşık 2 katı olduğu genel kuralı nasıl taklit ettiği. Ortalama O3 değeriniz O0 değerinizin% 51,70'inde çalışır. Alt / üst varyansları uyguladığınızda, (O3-200) / (O0 + 158386) var,% 51,67'ye yükselme görüyoruz.

Kısacası, evet, bir önbellek asla deterministik olmayacaktır; ve gördüğünüz düşük varyans, daha yavaş bir cihazla senkronize edilmesi beklenen şeyle uyumludur. Daha belirleyici olan yalnızca kayıt makinesine kıyasla sadece büyük bir varyanstır.


Talimatlar L1i önbelleğinden alınır. Sanırım bunun öngörülemeyen yavaşlamalardan muzdarip olamayacağını söylüyorsunuz çünkü aynı veya diğer çekirdeklerdeki veri önbellekleriyle tutarlı değil mi? Her neyse, Dr.Bantwidth'in cevabı doğruysa, varyans önbelleğin kendisinden değil, çekirdek tarafından periyodik dTLB geçersiz kılmasından kaynaklanır. Bu açıklama tüm gözlemi tam olarak açıklar: kullanıcı boşluğuna herhangi bir yük / depo eklemenin artan varyansı ve bir çekirdek modül içindeki döngü zamanlanırken bu düşüşün gerçekleşmemesi. (Linux çekirdek belleği değiştirilemez.)
Peter Cordes

Sıcak verilere erişirken önbellekler genellikle belirleyicidir. Çekirdekten gelen yükleri / depoları rahatsız etmeden tutarlılık trafiğine izin vermek için çok portatif olabilirler. Bozuklukların diğer çekirdeklerden kaynaklandığını tahmin etmek akla yatkın ama ben numa_balancingTLB'nin geçersiz kılmalarını muhtemelen açıklıyorum.
Peter Cordes

Herhangi bir gözetleme önbelleği, herhangi bir isteğin durdurulması gereken kesintisiz bir diziye sahip olmalıdır. 1'e 2 döngü işleminde 10 ^ -4'lük yavaşlama, her 10 ^ 5 işlemde bir saat hıçkırık anlamına gelir. Tüm soru gerçekten bir op-no, varyans küçük.
mevets
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.