Cortex-A72에서 -O0을 사용하지만 -O3을 사용하지 않는 단순 타이트 루프에 대해 사이클에서 이러한 변동성이 큰 이유는 무엇입니까?


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_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)입니다. 이 ARM PMU 참조 섹션 11.8을 사용하고 있습니다 ).


2
다른 스레드가 실행 중입니까? 버스 대역폭 및 캐시 공간에 대한 경쟁을 유발하는 메모리 액세스는 영향을 줄 수 있습니다.
prl

될 수 있습니다. 나는 어떤 코어도 격리하지 않았으며 심지어 커널 스레드는 소켓의 다른 코어 중 하나에서 예약 될 수 있습니다. 그러나 lscpu --extended올바르게 이해 한다면 각 코어에는 자체 L1 데이터 및 명령 캐시가 있고 각 소켓에는 4 개의 코어에 대한 공유 L2 캐시가 있습니다. 그것의 버스를 "소유"합니다 (완성 될 때까지 코어에서 실행되는 유일한 것이기 때문에). 그래도 나는이 수준의 하드웨어에 대해 잘 모른다.
sevko

1
네, 분명히 소켓 4 개로보고되었지만 16 코어 SoC 내부에서 인터커넥트가 어떻게 연결되어 있는지에 대한 문제 일 수 있습니다. 그러나 당신은 물리적 인 기계를 가지고 있습니다. 브랜드와 모델 번호가 있습니까? 뚜껑이 벗겨지면 실제로 4 개의 분리 된 소켓이 있는지 확인할 수도 있습니다. 그러나 mobo의 공급 업체 / 모델 번호를 제외하고는 왜 이것이 중요한지 알 수 없습니다. 벤치 마크는 순전히 단일 코어이며 캐시에서 핫 상태를 유지해야하므로 중요한 것은 A72 코어 자체와 저장소 버퍼 + 저장소 전달뿐입니다.
Peter Cordes

1
커널 모듈을 변경하여 세 개의 카운터를 추적하고 샘플 출력을 추가했습니다. 흥미로운 점은 대부분의 실행이 일관 적이지만 임의의 실행은 실질적으로 더 빠를 것입니다. 이 경우, 실제로 아주 약간했다 가장 빠른 것 같습니다 보다 어쩌면 더 공격적 분기 예측 곳을 의미 L1 액세스. 또한 불행히도 기계에 액세스 할 수 없습니다. AWS a1.metal 인스턴스입니다 (실제 하드웨어의 전체 소유권을 제공하므로 하이퍼 바이저 등의 간섭이 없을 수도 있습니다).
sevko

1
흥미롭게도, 커널 모듈이를 통해 모든 CPU에서이 코드를 동시에 실행하게하면 on_each_cpu(), 각각 100 번의 시험에서 거의 변하지 않는 것으로보고됩니다.
sevko

답변:


4

최근 Linux 커널에서 자동 NUMA 페이지 마이그레이션 메커니즘은 주기적으로 TLB 항목을 차단하여 NUMA 지역을 모니터링 할 수 있습니다. 데이터가 L1DCache에 남아 있어도 TLB 재로드로 인해 O0 코드가 느려집니다.

커널 페이지에서 페이지 마이그레이션 메커니즘을 활성화하지 않아야합니다.

자동 NUMA 페이지 마이그레이션이 사용 가능한지 확인합니다.

$ cat /proc/sys/kernel/numa_balancing

그리고 당신은 그것을 비활성화 할 수 있습니다

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

나는 최근에 관련 테스트를 해왔다. L1 캐시에 편안하게 맞는 메모리 버퍼에 무작위로 액세스하는 워크로드를 실행하고 있습니다. 나는 여러 번의 시련을 연속적으로 실행하며 주기적으로 작은 상승 스파이크가 있다는 것을 제외하고는 달리기 시간이 매우 일정합니다 (말 그대로 0.001 % 미만으로 다양 함). 이 급등으로 벤치 마크는 0.014 % 더 길어졌습니다. 이 크기는 작지만 이러한 각 스파이크의 크기는 정확히 동일하며 2 초마다 거의 한 번 스파이크가 발생합니다. 이 기계는 numa_balancing비활성화되었습니다. 아마도 당신은 아이디어가 있습니까?
세브 코

알아 냈습니다. 나는 하루 종일 perf 카운터를 쳐다보고 있었지만 근본 원인은 전혀 관련이 없었습니다. 나는 조용한 테스트를 위해 tmux 세션에서 이러한 테스트를 실행하고있었습니다. 2 초 간격은 tmux 상태 표시 줄의 새로 고침 간격과 정확히 일치하여 다른 것들 사이에 네트워크 요청을합니다. 비활성화하면 스파이크가 사라졌습니다. 다른 코어 클러스터에서 내 상태 표시 줄에 의해 실행 된 스크립트가 어떻게 L1 데이터 만
건드 리면서

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 무효화 때문입니다. 이 설명은 모든 관찰, 즉 사용자 공간에로드 / 스토어를 포함함으로써 발생하는 분산의 증가와 커널 모듈 내부의 루프 타이밍시이 드롭이 발생하지 않는다는 사실을 완전히 설명합니다. (리눅스 커널 메모리는
Peter Cordes

캐시는 일반적으로 핫 데이터에 액세스 할 때 결정적입니다. 코어 자체의로드 / 스토어를 방해하지 않으면 서 일관성있는 트래픽을 허용하도록 멀티 포트 될 수 있습니다. 교란이 다른 핵심으로 인한 것이라는 추측은 그럴듯 하지만 numa_balancingTLB 무효화만으로도 설명 할 수 있습니다.
Peter Cordes

스누핑 캐시는 요청이 중단되어야하는 무중단 시퀀스를 가져야합니다. 1 대 2 사이클 동작에서 10 ^ -4의 속도 저하는 10 ^ 5 동작마다 하나의 클록 하이커를 의미합니다. 전체 질문은 실제로는 아무런 문제가 아니며 분산은 작습니다.
mevets
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.