Копирование 64 байтов памяти с хранилищами NT в одну строку полного кэша против 2 последовательных строк неполного кэша - PullRequest
7 голосов
/ 18 января 2020

Я читаю Руководство по оптимизации Intel о записи объединения памяти и писал тесты, чтобы понять, как это работает. Это две функции, на которых я запускаю тесты:

memcopy.h:

void avx_ntcopy_cache_line(void *dest, const void *src);

void avx_ntcopy_64_two_cache_lines(void *dest, const void *src);

memcopy.S:

avx_ntcopy_cache_line:
    vmovdqa ymm0, [rdi]
    vmovdqa ymm1, [rdi + 0x20]
    vmovntdq [rsi], ymm0
    vmovntdq [rsi + 0x20], ymm1
    ;intentionally no sfence after nt-store
    ret

avx_ntcopy_64_two_cache_lines:
    vmovdqa ymm0, [rdi]
    vmovdqa ymm1, [rdi + 0x40]
    vmovntdq [rsi], ymm0
    vmovntdq [rsi + 0x40], ymm1
    ;intentionally no sfence after nt-store
    ret

Вот основная функция теста выглядит так:

#include <stdlib.h>
#include <inttypes.h>
#include <x86intrin.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include "memcopy.h"

#define ITERATIONS 1000000

//As @HadiBrais noted, there might be an issue with 4K aliasing
_Alignas(64) char src[128];
_Alignas(64) char dest[128];

static void run_benchmark(unsigned runs, unsigned run_iterations,
                    void (*fn)(void *, const void*), void *dest, const void* src);

int main(void){
    int fd = open("/dev/urandom", O_RDONLY);
    read(fd, src, sizeof src);

    run_benchmark(20, ITERATIONS, avx_ntcopy_cache_line, dest, src);
    run_benchmark(20, ITERATIONS, avx_ntcopy_64_two_cache_lines, dest, src);
}

static int uint64_compare(const void *u1, const void *u2){
    uint64_t uint1 = *(uint64_t *) u1;
    uint64_t uint2 = *(uint64_t *) u2;
    if(uint1 < uint2){
        return -1;
    } else if (uint1 == uint2){
        return 0;
    } else {
        return 1;
    }
}

static inline uint64_t benchmark_2cache_lines_copy_function(unsigned iterations, void (*fn)(void *, const void *),
                                               void *restrict dest, const void *restrict src){
    uint64_t *results = malloc(iterations * sizeof(uint64_t));
    unsigned idx = iterations;
    while(idx --> 0){
        uint64_t start = __rdpmc((1<<30)+1);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        fn(dest, src);
        uint64_t finish = __rdpmc((1<<30)+1);
        results[idx] = (finish - start) >> 4;
    }
    qsort(results, iterations, sizeof *results, uint64_compare);
    //median
    return results[iterations >> 1];
}

static void run_benchmark(unsigned runs, unsigned run_iterations,
                    void (*fn)(void *, const void*), void *dest, const void* src){
    unsigned current_run = 1;
    while(current_run <= runs){
        uint64_t time = benchmark_2cache_lines_copy_function(run_iterations, fn, dest, src);
        printf("Run %d result: %lu\n", current_run, time);
        current_run++;
    }
}

Компиляция с параметрами

-Werror \
-Wextra
-Wall \
-pedantic \
-Wno-stack-protector \
-g3 \
-O3 \
-Wno-unused-result \
-Wno-unused-parameter

И выполнение тестов я получил следующие результаты:

I. avx_ntcopy_cache_line:

Run 1 result: 61
Run 2 result: 61
Run 3 result: 61
Run 4 result: 61
Run 5 result: 61
Run 6 result: 61
Run 7 result: 61
Run 8 result: 61
Run 9 result: 61
Run 10 result: 61
Run 11 result: 61
Run 12 result: 61
Run 13 result: 61
Run 14 result: 61
Run 15 result: 61
Run 16 result: 61
Run 17 result: 61
Run 18 result: 61
Run 19 result: 61
Run 20 result: 61

perf:

 Performance counter stats for './bin':

     3 503 775 289      L1-dcache-loads                                               (18,87%)
        91 965 805      L1-dcache-load-misses     #    2,62% of all L1-dcache hits    (18,94%)
     2 041 496 256      L1-dcache-stores                                              (19,01%)
         5 461 440      LLC-loads                                                     (19,08%)
         1 108 179      LLC-load-misses           #   20,29% of all LL-cache hits     (19,10%)
        18 028 817      LLC-stores                                                    (9,55%)
       116 865 915      l2_rqsts.all_pf                                               (14,32%)
                 0      sw_prefetch_access.t1_t2                                      (19,10%)
           666 096      l2_lines_out.useless_hwpf                                     (19,10%)
        47 701 696      l2_rqsts.pf_hit                                               (19,10%)
        62 556 656      l2_rqsts.pf_miss                                              (19,10%)
         4 568 231      load_hit_pre.sw_pf                                            (19,10%)
        17 113 190      l2_rqsts.rfo_hit                                              (19,10%)
        15 248 685      l2_rqsts.rfo_miss                                             (19,10%)
        54 460 370      LD_BLOCKS_PARTIAL.ADDRESS_ALIAS                                     (19,10%)
    18 469 040 693      uops_retired.stall_cycles                                     (19,10%)
    16 796 868 661      uops_executed.stall_cycles                                     (19,10%)
    18 315 632 129      uops_issued.stall_cycles                                      (19,05%)
    16 176 115 539      resource_stalls.sb                                            (18,98%)
    16 424 440 816      resource_stalls.any                                           (18,92%)
    22 692 338 882      cycles                                                        (18,85%)

       5,780512545 seconds time elapsed

       5,740239000 seconds user
       0,040001000 seconds sys

II. avx_ntcopy_64_two_cache_lines:

Run 1 result: 6
Run 2 result: 6
Run 3 result: 6
Run 4 result: 6
Run 5 result: 6
Run 6 result: 6
Run 7 result: 6
Run 8 result: 6
Run 9 result: 6
Run 10 result: 6
Run 11 result: 6
Run 12 result: 6
Run 13 result: 6
Run 14 result: 6
Run 15 result: 6
Run 16 result: 6
Run 17 result: 6
Run 18 result: 6
Run 19 result: 6
Run 20 result: 6

perf:

 Performance counter stats for './bin':

     3 095 792 486      L1-dcache-loads                                               (19,26%)
        82 194 718      L1-dcache-load-misses     #    2,66% of all L1-dcache hits    (18,99%)
     1 793 291 250      L1-dcache-stores                                              (19,00%)
         4 612 503      LLC-loads                                                     (19,01%)
           975 438      LLC-load-misses           #   21,15% of all LL-cache hits     (18,94%)
        15 707 916      LLC-stores                                                    (9,47%)
        97 928 734      l2_rqsts.all_pf                                               (14,20%)
                 0      sw_prefetch_access.t1_t2                                      (19,21%)
           532 203      l2_lines_out.useless_hwpf                                     (19,19%)
        35 394 752      l2_rqsts.pf_hit                                               (19,20%)
        56 303 030      l2_rqsts.pf_miss                                              (19,20%)
         6 197 253      load_hit_pre.sw_pf                                            (18,93%)
        13 458 517      l2_rqsts.rfo_hit                                              (18,94%)
        14 031 767      l2_rqsts.rfo_miss                                             (18,93%)
        36 406 273      LD_BLOCKS_PARTIAL.ADDRESS_ALIAS                                     (18,94%)
     2 213 339 719      uops_retired.stall_cycles                                     (18,93%)
     1 225 185 268      uops_executed.stall_cycles                                     (18,94%)
     1 943 649 682      uops_issued.stall_cycles                                      (18,94%)
       126 401 004      resource_stalls.sb                                            (19,20%)
       202 537 285      resource_stalls.any                                           (19,20%)
     5 676 443 982      cycles                                                        (19,18%)

       1,521271014 seconds time elapsed

       1,483660000 seconds user
       0,032253000 seconds sys

Как видно, разница в результатах измерений в 10 раз.


Моя интерпретация :

Как объяснено в Intel Optimization Manual/3.6.9:

записи в различные части одной и той же строки кэша могут быть сгруппированы в одну транзакцию шины с полной строкой кэша вместо перехода по шине (поскольку они не кэшируются), так как несколько частичных записей

Я предположил, что в случае avx_ntcopy_cache_line у нас есть полная 64-байтовая запись, инициирующая транзакцию шины для их записи, которая запрещает rdtsc быть казненным t порядка.

В отличие от этого, в случае avx_ntcopy_64_two_cache_lines мы записали 32 байта в разные строки кэша, идущие в буфер W C, и шинная транзакция не была запущена. Это позволило rdtsc быть выполненным не по порядку.

Эта интерпретация выглядит крайне подозрительно, и она не go наряду с bus-cycles разницей:

avx_ntcopy_cache_line: 131 454 700

avx_ntcopy_64_two_cache_lines: 31 957 050

ВОПРОС: Какова истинная причина такой разницы в измерениях?

1 Ответ

4 голосов
/ 19 января 2020

Гипотеза: (полностью) перекрывающееся хранилище с еще не очищенным буфером W C может просто слиться с ним. Завершение строки вызывает немедленный грипп sh, и все те магазины, которые уходят из ядра, работают медленно.

Вы сообщаете в 100 раз больше resource_stalls.sb для полной версии, чем для версии с двумя частичными линиями , Это согласуется с этим объяснением.

Если 2_lines могут зафиксировать хранилища NT в существующих буферах W C (LFB), буфер хранилища может идти в ногу со скоростью выполнения инструкций хранилища, обычно узким местом в чем-то другом. (Вероятно, только внешний интерфейс, учитывая издержки call / ret для каждой пары нагрузок / хранилищ. Хотя, конечно, call включает в себя магазин.) Ваши результаты perf показывают 1,8 миллиарда магазинов (до L1) более 5,7 миллиарда циклов, так что в пределах 1 магазина / цикла мы можем ожидать, что магазины будут загружены в буфер W C.

Но если буферы W C сбрасываются , что происходит, когда строка полностью записана, ей нужно go от ядра (что медленно), на некоторое время связывая этот LFB, чтобы его нельзя было использовать для фиксации последующих хранилищ NT . Когда хранилища не могут покинуть буфер хранилища, он заполняется, и ядро ​​останавливается при возможности выделить ресурсы для новых инструкций хранилища для входа в серверную часть. (В частности, проблема / переименование / выделение сценических остановок.)

Вы, вероятно, могли бы видеть этот эффект более отчетливо с любым из событий L2, L3, SQ, offcore req / resp, которые бы собирали весь этот трафик c за пределами L1. Вы включаете некоторые счетчики L2, но те, вероятно, не используют хранилище NT, которое проходит через L2.


Улучшенный REP MOVSB ​​для memcpy предлагает , чтобы хранилища NT занимали больше времени чтобы LFB «передавал» на внешние уровни иерархии памяти , оставляя LFB занятым еще долго после начала запроса. (Возможно, чтобы убедиться, что ядро ​​всегда может перезагрузить то, что оно только что сохранило, или иначе не потерять след хранилища NT в полете, чтобы поддерживать согласованность с MESI.) Более позднему sfence также необходимо знать, когда стали видны более ранние хранилища NT. для других ядер, поэтому мы не можем иметь их невидимыми в любой момент до этого.

Даже если это не так, все равно будет где-то узкое место пропускной способности для всех этих запросов хранилища NT. Таким образом, другой возможный механизм заключается в том, что они заполняют некоторый буфер, и тогда ядро ​​больше не может передавать LFB, поэтому у него заканчивается LFB для фиксации хранилищ NT, а затем SB заполняет задержку выделения.

Они могут слиться, как только доберутся до контроллера памяти, и каждому не потребуется пакетная передача по фактической внешней шине памяти, но путь от ядра через ядро ​​к контроллеру памяти не будет коротким.


Даже выполнение 2x rdpmc для каждых 32 хранилищ недостаточно замедляет процессор, чтобы предотвратить заполнение буфера хранилища; то, что вы видите, зависит от выполнения этого в относительно узком l oop, а не от однократного выполнения с пустым буфером хранения для начала. Кроме того, ваше предположение, что rdpmc или rdtsc не будет переупорядочено по отношению к. очистка буферов W C не имеет смысла. Оформление магазинов не заказано в отношении. выполнение rdtsc.

TL: DR: ваше rdpmc время для отдельной группы магазинов бесполезно, и, если что-то скрывает некоторую разницу в производительности, замедляя быстрый случай это не является узким местом в буфере хранилища.

...