อะไรทำให้เกิดความแปรปรวนสูงในรอบนี้สำหรับการวนรอบแบบเรียบง่ายด้วย -O0 แต่ไม่ใช่ -O3 บน Cortex-A72


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ฉันเห็นช่วงโดยเฉลี่ยประมาณ 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

สำหรับแต่ละการทดลองค่าที่รายงานคือ: # of cycles (0x11), # ของ L1D accesses (0x04), # of L1I accesses (0x14) ฉันใช้ส่วนที่ 11.8 ของการอ้างอิง ARM PMU นี้ )


2
มีเธรดอื่นทำงานหรือไม่ การเข้าถึงหน่วยความจำทำให้เกิดการแข่งขันสำหรับแบนด์วิดท์บัสและพื้นที่แคชอาจมีผลกระทบ
prl

อาจจะเป็น. ฉันยังไม่ได้แยกคอร์ใด ๆ และแม้กระทั่งแล้วเคอร์เนลเธรดอาจถูกกำหนดเวลาไว้ในคอร์อื่นในซ็อกเก็ต แต่ถ้าฉันเข้าใจlscpu --extendedอย่างถูกต้องแล้วแต่ละคอร์มีข้อมูล L1 และแคชคำสั่งของตัวเองแล้วซ็อกเก็ตแต่ละอันมีแคช L2 ที่แชร์สำหรับ 4 คอร์ดังนั้นตราบใดที่ทุกอย่างทำภายในแคช L1 ฉันคาดหวังว่าโค้ดจะสวย มาก "เจ้าของ" รถบัสของมัน (เพราะมันเป็นสิ่งเดียวที่ทำงานบนแกนของมันจนเสร็จสมบูรณ์) ฉันไม่ทราบเกี่ยวกับฮาร์ดแวร์ในระดับนี้มากนัก
sevko

1
ใช่มันมีการรายงานอย่างชัดเจนว่าเป็น 4 ซ็อกเก็ต แต่นั่นอาจเป็นเรื่องของการเชื่อมต่อระหว่างกันที่เชื่อมต่อเข้ากับ SoC 16-core แต่คุณมีเครื่องทางกายภาพใช่มั้ย คุณมียี่ห้อและหมายเลขรุ่นหรือไม่ หากฝาปิดดับลงคุณสามารถยืนยันได้ว่ามีซ็อกเก็ต 4 ตัวแยกกันจริง ๆ หรือไม่ ฉันไม่เห็นว่าทำไมสิ่งนี้จะเกิดขึ้นยกเว้นสำหรับผู้ขาย / หมายเลขรุ่นของ mobo เบนช์มาร์กของคุณนั้นมีแกนเดี่ยวล้วนๆและน่าจะอยู่ในแคชดังนั้นสิ่งที่สำคัญก็คือตัว A72 และตัวบัฟเฟอร์บัฟเฟอร์ + การส่งต่อที่เก็บ
Peter Cordes

1
ฉันเปลี่ยนโมดูลเคอร์เนลเพื่อติดตามสามตัวนับและเพิ่มเอาต์พุตตัวอย่างบางส่วน สิ่งที่น่าสนใจคือการวิ่งส่วนใหญ่นั้นสอดคล้องกัน แต่จากนั้นการสุ่มจะเร็วขึ้นอย่างมาก ในกรณีนี้ดูเหมือนว่าเร็วที่สุดที่จริงแล้วมีการเข้าถึง L1 เพิ่มขึ้นเล็กน้อยซึ่งอาจหมายถึงการทำนายสาขาที่ก้าวร้าวมากขึ้นบางแห่ง นอกจากนี้น่าเสียดายที่ฉันไม่สามารถเข้าถึงเครื่องได้ เป็นอินสแตนซ์ของ AWS a1.metal (ซึ่งให้สิทธิ์คุณเต็มในการเป็นเจ้าของฮาร์ดแวร์ทางกายภาพดังนั้นจึงไม่มีสัญญาณรบกวนจากไฮเปอร์ไวเซอร์ ฯลฯ )
sevko

1
ที่น่าสนใจถ้าฉันทำให้เคอร์เนลโมดูลรันโค้ดนี้บน CPU ทั้งหมดพร้อมกันผ่าน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 ของฉันซึ่งทำให้คำขอเครือข่ายเหนือสิ่งอื่นใดการปิดใช้งานทำให้ spikes หายไป ไม่มีความคิดว่าสคริปต์ที่ทำงานโดยบรรทัดสถานะของฉันในคลัสเตอร์หลักที่แตกต่างกันนั้นส่งผลกระทบต่อกระบวนการที่ทำงานในคลัสเตอร์หลักที่แยกได้เพียงสัมผัสข้อมูล L1 เท่านั้น ..
sevko

2

ความแปรปรวนของคุณอยู่ในอันดับที่ 6 * 10 ^ -4 ในขณะที่น่าตกใจมากกว่า 1.3 * 10 ^ -6 เมื่อโปรแกรมของคุณพูดคุยกับแคชมันมีส่วนเกี่ยวข้องกับการดำเนินการที่ทำข้อมูลให้ตรงกันหลายอย่าง การซิงโครไนซ์หมายถึงเสียเวลาเสมอ

สิ่งที่น่าสนใจคือการเปรียบเทียบ -O0, -O3 ของคุณเลียนแบบกฎทั่วไปที่ L1-cache-hit นั้นเกี่ยวกับการอ้างอิงการลงทะเบียน 2x O3 เฉลี่ยของคุณทำงานใน 51.70% ของเวลาที่ O0 ทำ เมื่อคุณใช้ความแปรปรวนต่ำกว่า / ตัวพิมพ์ใหญ่เรามี (O3-200) / (O0 + 158386) เราจะเห็นการปรับปรุงที่ 51.67%

กล่าวโดยย่อคือใช่ว่าแคชจะไม่ถูกกำหนด และความแปรปรวนต่ำที่คุณเห็นนั้นสอดคล้องกับสิ่งที่ควรคาดหวังจากการซิงโครไนซ์กับอุปกรณ์ที่ช้ากว่า มันเป็นเพียงความแปรปรวนขนาดใหญ่เมื่อเปรียบเทียบกับเครื่องลงทะเบียนที่กำหนดค่าได้มากกว่าเท่านั้น


ได้รับคำแนะนำจากแคช L1i ฉันเดาว่าคุณกำลังบอกว่าไม่สามารถทนทุกข์ทรมานจากการชะลอตัวที่คาดเดาไม่ได้เพราะมันไม่สอดคล้องกับแคชข้อมูลในแกนเดียวกันหรือแกนอื่น ๆ ? แต่อย่างไรก็ตามถ้าคำตอบของดร. แบนด์วิดท์นั้นถูกต้องความแปรปรวนไม่ได้เกิดจากการแคชเอง แต่เป็นการทำให้ dTLB เป็นโมฆะเป็นระยะโดยเคอร์เนล คำอธิบายนั้นอธิบายการสังเกตทั้งหมดอย่างสมบูรณ์: ความแปรปรวนที่เพิ่มขึ้นจากการรวมโหลด / ร้านค้าในพื้นที่ผู้ใช้และความจริงที่ว่าการปล่อยนี้จะไม่เกิดขึ้นเมื่อกำหนดเวลาวนรอบภายในโมดูลเคอร์เนล (หน่วยความจำเคอร์เนลลินุกซ์ไม่สามารถทำการเปลี่ยนได้)
Peter Cordes

โดยทั่วไปแคชจะถูกกำหนดไว้เมื่อคุณเข้าถึงข้อมูลที่ร้อนแรง พวกเขาสามารถหลายพอร์ตเพื่อให้การรับส่งข้อมูลความสอดคล้องกันโดยไม่รบกวนโหลด / ร้านค้าจากแกนตัวเอง คุณเดาว่าการรบกวนเกิดจากคอร์อื่น ๆ เป็นไปได้แต่ฉันnuma_balancingTLB การตรวจสอบความถูกต้องเพียงอย่างเดียวอาจอธิบายได้
Peter Cordes

แคชสอดแนมใด ๆ จะต้องมีลำดับที่ไม่สามารถขัดจังหวะโดยที่คำขอใด ๆ จะต้องหยุดทำงาน การชะลอตัวของ 10 ^ -4 ในการดำเนินงาน 1 vs 2 รอบหมายถึงหนึ่งนาฬิกา hiccough ทุก ๆ 10 ^ 5 การดำเนินงาน คำถามทั้งหมดเป็นจริงไม่มีความแปรปรวนมีขนาดเล็ก
mevets
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.