Що обумовлює цю велику мінливість циклів для простого тугого циклу з -00, але не -O3, на Cortex-A72?


9

Я проводжу кілька експериментів, щоб отримати дуже послідовний час виконання для фрагмента коду. Код, який я зараз призначаю, є досить довільним навантаженням на процесор:

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, я бачив діапазон щонайменше 200 циклів із ~ 135,845,192 в середньому, при цьому більшість випробувань займає рівно стільки ж часу. Однак при компіляції з -O0дальності діапазон вистрілив до 158,386 циклів із ~ 262,710,916. Під діапазоном я маю на увазі різницю між найдовшими та найкоротшими часом роботи. Більше того, що стосується -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_O0і cpu_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

Зразок вимірювань

Нижче наведено деякий вихід з одного виконання модуля ядра:

[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). Я використовую розділ 11.8 цього посилання на ARM PMU ).


2
Чи працюють інші потоки? Їх доступ до пам'яті, що викликає конкуренцію за пропускну здатність шини та простір кешу, може мати ефект.
prl

Може бути. У мене немає isolcpu'd жодних ядер, і навіть тоді ядра ядра можуть заплануватись на одній з інших ядер сокета. Але якщо я lscpu --extendedправильно розумію , то кожне ядро ​​має власні кеші даних L1 та кеш інструкцій, і тоді кожен сокет має спільний кеш L2 для своїх 4 ядер, так що, поки все зроблено в кеш-пам'яті L1, я очікую, що код буде досить набагато "володіє" своєю шиною (оскільки це єдине, що працює на її ядрі, до завершення). Я не знаю багато про обладнання на цьому рівні.
sevko

1
Так, це чітко повідомляється про 4 розетки, але це може бути лише питанням того, як з'єднання з'єднано всередині 16-ядерного SoC. Але у вас є фізична машина, правда? У вас є бренд і номер моделі для нього? Якщо кришка відімкнеться, імовірно, ви також можете підтвердити, чи справді є 4 окремі розетки. Я не бачу, чому б щось із цього мало значення, за винятком, можливо, номера продавця / моделі мобо. Ваш орієнтир суто одноядерний і повинен залишатися гарячим в кеші, тому все, що має значення, - це саме ядро ​​A72 та його буфер зберігання + пересилання в магазин.
Пітер Кордес

1
Я змінив модуль ядра, щоб відстежувати три лічильники, і додав вибірки. Цікаво, що більшість прогонів послідовні, але тоді випадковий буде значно швидшим. У цьому випадку схоже на те, що найшвидший фактично мав дещо більше доступу L1, що, можливо, передбачає дещо агресивніший прогноз гілки. Також, на жаль, у мене немає доступу до машини. Це екземпляр AWS a1.metal (який дає вам повне володіння фізичним обладнанням, тому нібито ніяких втручань з боку гіпервізора тощо).
sevko

1
Цікаво, що якщо я змушую модуль ядра запускати цей код на всіх процесорах одночасно через on_each_cpu()кожен, то кожен повідомляє про майже відсутність змін у 100 випробувань.
sevko

Відповіді:


4

В останніх ядрах Linux механізм автоматичної міграції сторінок NUMA періодично збиває записи TLB, щоб він міг контролювати локалізацію NUMA. Перезавантаження TLB уповільнить код O0, навіть якщо дані залишаються в L1DCache.

Механізм міграції сторінок не повинен бути активований на сторінках ядра.

Ви перевіряєте, чи включена автоматична міграція сторінок NUMA

$ cat /proc/sys/kernel/numa_balancing

і ви можете відключити його

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

Останнім часом я робив кілька пов'язаних тестувань. Я виконую робоче навантаження, яка робить купу випадкових доступів до буфера пам'яті, який зручно вписується в кеш L1. Я провожу купу випробувань назад до спини, і час роботи дуже послідовний (коливається буквально менше 0,001%), за винятком періодичних випадків, коли спостерігається невеликий вгору сплеск. У цьому сплеску тест працює лише на 0,014% довше. Це мало, але кожен із цих шипів має рівномірну величину, і спайк виникає один раз майже рівно кожні 2 секунди. Ця машина numa_balancingвідключена. Можливо, у вас є ідея?
sevko

Зрозумів це. Я цілий день дивився на лічильники парфумів, але, виявляється, першопричиною було щось зовсім не пов’язане між собою. Я проводив ці тести на сесії tmux на тихій машині. Інтервал 2 секунди точно збігся з інтервалом оновлення мого статусу tmux, що робить мережевий запит серед інших речей. Якщо його відключити, змусили зникнути шипи. Не маю уявлення, як скрипти, запущені моєю лінією статусу на іншому ядрі кластеру, впливали на процес, що працює на ізольованому ядро ​​кластера, торкаючись лише даних L1 ..
sevko

2

Ваша дисперсія в порядку 6 * 10 ^ -4. Хоча шокуюче більше 1,3 * 10 ^ -6, як тільки ваша програма розмовляє з кешами, вона бере участь у багатьох синхронізованих операціях. Синхронізація завжди означає витрачений час.

Цікавим є те, як ваше порівняння -O0, -O3 імітує загальне правило про те, що хіт L1-кешу приблизно 2 рази на посилання на регістр. Ваш середній O3 працює за 51,70% часу, який займає ваш O0. Коли ви застосовуєте нижню / верхню відхилення, ми маємо (O3-200) / (O0 + 158386), ми спостерігаємо поліпшення до 51,67%.

Якщо коротко, так, кеш ніколи не буде детермінованим; і низька дисперсія, яку ви бачите, відповідає тому, що слід очікувати від синхронізації з повільнішим пристроєм. Це лише велика дисперсія в порівнянні з більш детермінованою машиною, що працює лише в регістрі.


Інструкції отримують з кешу L1i. Я думаю, ти кажеш, що не може постраждати від непередбачуваних уповільнень, оскільки це не узгоджується з кешами даних на тих самих чи інших ядрах? Але в будь-якому випадку, якщо відповідь доктора Bandwidth правильна, відхилення викликано не самим кешем, а скоріше періодичним відключенням dTLB ядром. Це пояснення повністю пояснює все спостереження: збільшується відхилення від включення будь-яких навантажень / запасів у користувальницький простір, і той факт, що цього падіння не відбувається під час синхронізації циклу всередині модуля ядра. (Пам'ять ядра Linux не може бути замінена.)
Пітер Кордес

Кеші, як правило, детерміновані, коли ви отримуєте доступ до гарячих даних. Вони можуть бути багатопортовими, щоб забезпечити когерентність трафіку, не порушуючи навантажень / сховищ від самого ядра. Ви здогадуєтесь, що порушення пов'язані з іншими ядрами правдоподібно, але я, numa_balancingмабуть, лише інвалідність TLB пояснює це.
Пітер Кордес

Будь-який кешируючий кеш повинен мати безперебійну послідовність, в якій будь-який запит повинен бути зупинений. Уповільнення 10 ^ -4 в операції циклу 1 проти 2 означає один тактовий час ікокаш кожні 10 ^ 5 операцій. Ціле запитання насправді є неоперативним, дисперсія крихітна.
mevets
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.