Điều gì gây ra sự thay đổi cao này trong các chu kỳ cho một vòng lặp chặt chẽ đơn giản với -O0 nhưng không phải -O3, trên Cortex-A72?


9

Tôi đang chạy một số thử nghiệm xung quanh việc có được thời gian chạy nhất quán cao cho một đoạn mã. Mã tôi hiện đang định thời là một khối lượng công việc khá tùy ý CPU:

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

Tôi đã viết một mô-đun hạt nhân vô hiệu hóa các ngắt và sau đó chạy 10 thử nghiệm của chức năng trên, định thời gian cho mỗi thử nghiệm bằng cách lấy sự khác biệt trong bộ đếm chu kỳ đồng hồ từ trước và sau. Những điều khác cần lưu ý:

  • Máy là ARM Cortex-A72, có 4 ổ cắm với 4 lõi mỗi lõi (mỗi lõi có bộ đệm L1 riêng)
  • thang đo tần số đồng hồ bị tắt
  • siêu phân luồng không được hỗ trợ
  • Máy chạy hầu như không có gì ngoại trừ một số quy trình hệ thống xương trần

Nói cách khác, tôi tin rằng hầu hết / tất cả các nguồn biến đổi hệ thống đều được tính đến và đặc biệt là khi chạy như một mô-đun hạt nhân bị gián đoạn thông qua spin_lock_irqsave(), mã sẽ đạt được hiệu năng chạy khá giống nhau (có thể là một cú đánh hiệu năng nhỏ trong lần chạy đầu tiên khi một số lệnh đầu tiên được kéo vào bộ đệm, nhưng đó là nó).

Thật vậy, khi mã điểm chuẩn được biên dịch -O3, tôi đã thấy một phạm vi tối đa 200 chu kỳ trong số trung bình ~ 135.845.192, với hầu hết các thử nghiệm mất chính xác cùng một khoảng thời gian. Tuy nhiên , khi được biên dịch -O0, phạm vi bắn lên tới 158.386 chu kỳ trong số ~ 262.710.916. Theo phạm vi tôi có nghĩa là sự khác biệt giữa thời gian chạy dài nhất và ngắn nhất. Hơn nữa, đối với -O0mã, không có nhiều sự nhất quán trong đó các thử nghiệm là chậm nhất / nhanh nhất - ngược lại, trong một trường hợp, nhanh nhất là lần đầu tiên và chậm nhất là ngay sau đó!

Vì vậy : điều gì có thể gây ra giới hạn trên cao này về tính biến đổi trong -O0mã? Nhìn vào hội đồng, có vẻ như -O3mã lưu trữ mọi thứ (?) Trong một thanh ghi, trong khi -O0mã có một loạt các tham chiếu đến spvà do đó dường như nó đang truy cập vào bộ nhớ. Nhưng ngay cả sau đó, tôi hy vọng mọi thứ sẽ được đưa vào bộ đệm L1 và ngồi ở đó với thời gian truy cập khá xác định.


Mã đang được điểm chuẩn nằm trong đoạn trích ở trên. Việc lắp ráp dưới đây. Cả hai đều được biên dịch mà gcc 7.4.0không có cờ ngoại trừ -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

mô-đun hạt nhân

Mã chạy thử nghiệm dưới đây. Nó đọc PMCCNTR_EL0trước / sau mỗi lần lặp, lưu trữ sự khác biệt trong một mảng và in ra số lần tối thiểu / tối đa cuối cùng trong tất cả các thử nghiệm. Các hàm cpu_workload_external_O0cpu_workload_external_O3trong các tệp đối tượng bên ngoài được biên dịch riêng, và sau đó được liên kết trong.

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

Phần cứng

$ 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

Các phép đo mẫu

Dưới đây là một số đầu ra từ một lần thực hiện mô-đun hạt nhân:

[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

Đối với mỗi thử nghiệm, các giá trị được báo cáo là: # of chu kỳ (0x11), # trong số truy cập L1D (0x04), # trong số truy cập L1I (0x14). Tôi đang sử dụng phần 11.8 của tài liệu tham khảo PMU ARM này ).


2
Có chủ đề khác đang chạy? Bộ nhớ của họ truy cập gây ra sự cạnh tranh về băng thông xe buýt và không gian bộ nhớ cache có thể có ảnh hưởng.
prl

Có thể là. Tôi đã không cô lập bất kỳ lõi nào, và thậm chí sau đó một chuỗi nhân có thể được lên lịch trên một trong các lõi khác trên ổ cắm. Nhưng nếu tôi hiểu lscpu --extendedchính xác, thì mỗi lõi có bộ nhớ dữ liệu và lệnh L1 riêng, và sau đó mỗi ổ cắm có bộ đệm L2 được chia sẻ cho 4 lõi của nó, miễn là mọi thứ được thực hiện trong bộ đệm L1, tôi sẽ mong mã sẽ đẹp nhiều "sở hữu" xe buýt của nó (vì đó là thứ duy nhất chạy trên lõi của nó, cho đến khi hoàn thành). Tôi không biết nhiều về phần cứng ở cấp độ này.
Sevko

1
Vâng, nó được báo cáo rõ ràng là 4 ổ cắm, nhưng đó có thể chỉ là vấn đề làm thế nào kết nối được nối với nhau trong SoC 16 lõi. Nhưng bạn có máy vật lý, phải không? Bạn có một thương hiệu và số mô hình cho nó? Nếu nắp bật ra, có lẽ bạn cũng có thể xác nhận xem có thực sự có 4 ổ cắm riêng biệt hay không. Tuy nhiên, tôi không thấy lý do tại sao bất kỳ điều này sẽ quan trọng, ngoại trừ có thể là số nhà cung cấp / kiểu máy của mobo. Điểm chuẩn của bạn hoàn toàn là lõi đơn và nên nóng trong bộ nhớ cache, vì vậy tất cả những gì quan trọng là chính lõi A72 và bộ đệm lưu trữ + chuyển tiếp lưu trữ của nó.
Peter Cordes

1
Tôi đã thay đổi mô-đun hạt nhân để theo dõi ba bộ đếm và thêm một số đầu ra mẫu. Điều thú vị là hầu hết các lần chạy đều nhất quán, nhưng sau đó một lần chạy ngẫu nhiên sẽ nhanh hơn đáng kể. Trong trường hợp này, có vẻ như người nhanh nhất thực sự có nhiều truy cập L1 hơn một chút , điều này có thể ngụ ý dự đoán nhánh mạnh hơn ở đâu đó. Ngoài ra, thật không may, tôi không có quyền truy cập vào máy. Đó là một phiên bản AWS a1.metal (cung cấp cho bạn toàn quyền sở hữu phần cứng vật lý, do đó, rõ ràng là không có sự can thiệp nào từ một trình ảo hóa, v.v.).
Sevko

1
Thật thú vị, nếu tôi làm cho mô-đun hạt nhân chạy mã này trên tất cả các CPU đồng thời thông qua on_each_cpu(), mỗi một báo cáo hầu như không có biến đổi trong tất cả 100 thử nghiệm.
Sevko

Câu trả lời:


4

Trong các nhân Linux gần đây, cơ chế di chuyển trang NUMA tự động định kỳ bắn ra các mục TLB để nó có thể giám sát địa phương NUMA. Tải lại TLB sẽ làm chậm mã O0, ngay cả khi dữ liệu vẫn còn trong L1DCache.

Cơ chế di chuyển trang không nên được kích hoạt trên các trang kernel.

Bạn kiểm tra xem liệu di chuyển trang NUMA tự động có được bật hay không

$ cat /proc/sys/kernel/numa_balancing

và bạn có thể vô hiệu hóa nó với

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

Gần đây tôi đã làm một số thử nghiệm liên quan. Tôi đang chạy một khối lượng công việc làm cho một loạt các truy cập ngẫu nhiên vào một bộ nhớ đệm phù hợp thoải mái trong bộ đệm L1. Tôi chạy một loạt các thử nghiệm trở lại và thời gian chạy rất nhất quán (thay đổi theo nghĩa đen nhỏ hơn 0,001%), ngoại trừ định kỳ có một sự tăng vọt nhỏ. Trong đó tăng đột biến điểm chuẩn chỉ chạy 0,011%. Điều này là nhỏ, nhưng mỗi gai này có độ lớn chính xác như nhau, và một đột biến xảy ra một lần gần như chính xác cứ sau 2 giây. Máy này đã numa_balancingbị vô hiệu hóa. Có lẽ bạn có một ý tưởng?
Sevko

Tìm ra. Tôi đã nhìn chằm chằm vào quầy hoàn hảo cả ngày nhưng hóa ra nguyên nhân gốc rễ là thứ gì đó hoàn toàn không liên quan .. Tôi đã chạy các thử nghiệm này trong một phiên tmux trên một chiếc máy yên tĩnh. Khoảng thời gian 2 giây trùng khớp chính xác với khoảng thời gian làm mới của dòng trạng thái tmux của tôi, điều này khiến cho một yêu cầu mạng trong số một số thứ khác .. Vô hiệu hóa nó làm cho các xung đột biến mất. Không biết làm thế nào các tập lệnh chạy bởi dòng trạng thái của tôi trên một cụm lõi khác đã ảnh hưởng đến quá trình chạy trên một cụm lõi bị cô lập, chỉ chạm vào dữ liệu L1 ..
Sevko

2

Phương sai của bạn theo thứ tự 6 * 10 ^ -4. Mặc dù đáng kinh ngạc hơn 1,3 * 10 ^ -6, một khi chương trình của bạn đang nói chuyện với bộ nhớ cache, nó có liên quan đến nhiều hoạt động được đồng bộ hóa. Đồng bộ hóa luôn có nghĩa là lãng phí thời gian.

Một điều thú vị là cách so sánh -O0, -O3 của bạn bắt chước quy tắc chung rằng một lần truy cập bộ đệm L1 là khoảng 2 lần một tham chiếu đăng ký. O3 trung bình của bạn chạy trong 51,70% thời gian O0 của bạn làm. Khi bạn áp dụng phương sai thấp hơn / cao hơn, chúng tôi có (O3-200) / (O0 + 158386), chúng tôi thấy sự cải thiện lên 51,67%.

Nói tóm lại, vâng, một bộ đệm sẽ không bao giờ mang tính quyết định; và phương sai thấp mà bạn thấy phù hợp với những gì sẽ được mong đợi từ việc đồng bộ hóa với một thiết bị chậm hơn. Nó chỉ là một phương sai lớn khi so sánh với máy chỉ đăng ký xác định hơn.


Hướng dẫn được lấy từ bộ đệm L1i. Tôi đoán bạn đang nói rằng không thể chịu đựng sự chậm chạp khó lường vì nó không kết hợp với bộ đệm dữ liệu trên cùng hoặc các lõi khác? Nhưng dù sao đi nữa, nếu câu trả lời của Tiến sĩ Băng thông là đúng, phương sai không phải do chính bộ đệm, mà là do sự vô hiệu hóa dTLB định kỳ của hạt nhân. Giải thích đó giải thích đầy đủ tất cả các quan sát: phương sai gia tăng bao gồm mọi tải / lưu trữ trong không gian người dùng và thực tế là sự sụt giảm này không xảy ra khi định thời vòng lặp trong mô-đun hạt nhân. (Bộ nhớ nhân Linux không thể hoán đổi được.)
Peter Cordes

Bộ nhớ cache thường mang tính quyết định khi bạn truy cập dữ liệu nóng. Chúng có thể được đa cổng để cho phép lưu lượng kết hợp mà không làm ảnh hưởng đến tải / cửa hàng từ chính lõi. Bạn đoán rằng sự xáo trộn là do các lõi khác là hợp lý nhưng tôi numa_balancingchỉ có thể làm mất hiệu lực của TLB.
Peter Cordes

Bất kỳ bộ đệm snooping nào cũng phải có một chuỗi liên tục trong đó mọi yêu cầu phải bị đình trệ. Chậm lại 10 ^ -4 trong hoạt động chu kỳ 1 vs 2 có nghĩa là một tiếng đồng hồ cứ sau 10 ^ 5 thao tác. Toàn bộ câu hỏi thực sự là không có, phương sai rất nhỏ.
mevets
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.