Я анализирую реализацию memcpy
на моей машине. Kaby Lake i7 снижается до __memcpy_avx_unaligned_erms
. Значение __x86_shared_non_temporal_threshold
, которое используется для определения необходимости использования невременных хранилищ AVX, составляет 6 МБ на моей машине. Фрагмент кода для не временных значений таков:
0x000000000018ee21 <+849>: prefetcht0 -0x100(%rcx)
prefetcht0 -0x140(%rcx)
prefetcht0 -0x180(%rcx)
prefetcht0 -0x1c0(%rcx)
vmovdqu (%rcx),%ymm0
vmovdqu -0x20(%rcx),%ymm1
vmovdqu -0x40(%rcx),%ymm2
vmovdqu -0x60(%rcx),%ymm3
sub $0x80,%rcx
sub $0x80,%rdx
vmovntdq %ymm0,(%r9)
vmovntdq %ymm1,-0x20(%r9)
vmovntdq %ymm2,-0x40(%r9)
vmovntdq %ymm3,-0x60(%r9)
sub r9,0x80
cmp rdx,0x80
ja 0x18ee21 <__memmove_avx_unaligned_erms+849>
ВОПРОС: Действительно ли prefetcht0
действительно полезен? У нас уже есть предварительная выборка HW, поэтому отсутствие предварительной выборки SW не должно вызывать большой разницы.
Я написал свою собственную функцию копирования для измерения разницы производительности с prefetcht0
и без него. Для простоты я предположил, что источник и назначение выровнены по страницам, а их размер кратен размеру страницы (это, скорее всего, будет справедливо для ввода-вывода).
Существует 2 реализации:
I. avx_copy_unr15
без предварительной выборки ПО
Декларация:
void *avx_copy_unr15(void *restrict dest, const void *restrict src, size_t sz);
Реализация:
//rdi - dest
//rsi - src
//rdx - size
avx_copy_unr15:
cmp $0, %rdx
jz avx_copy_unr15_ret
avx_copy_unr15_loop:
vmovapd (%rsi), %ymm0
vmovapd 0x20(%rsi), %ymm1
vmovapd 0x40(%rsi), %ymm2
vmovapd 0x60(%rsi), %ymm3
vmovapd 0x80(%rsi), %ymm4
vmovapd 0xA0(%rsi), %ymm5
vmovapd 0xC0(%rsi), %ymm6
vmovapd 0xE0(%rsi), %ymm7
vmovapd 0x100(%rsi), %ymm8
vmovapd 0x120(%rsi), %ymm9
vmovapd 0x140(%rsi), %ymm10
vmovapd 0x160(%rsi), %ymm11
vmovapd 0x180(%rsi), %ymm12
vmovapd 0x1A0(%rsi), %ymm13
vmovapd 0x1C0(%rsi), %ymm14
vmovapd 0x1E0(%rsi), %ymm15
vmovntpd %ymm0, (%rdi)
vmovntpd %ymm1, 0x20(%rdi)
vmovntpd %ymm2, 0x40(%rdi)
vmovntpd %ymm3, 0x60(%rdi)
vmovntpd %ymm4, 0x80(%rdi)
vmovntpd %ymm5, 0xA0(%rdi)
vmovntpd %ymm6, 0xC0(%rdi)
vmovntpd %ymm7, 0xE0(%rdi)
vmovntpd %ymm8, 0x100(%rdi)
vmovntpd %ymm9, 0x120(%rdi)
vmovntpd %ymm10, 0x140(%rdi)
vmovntpd %ymm11, 0x160(%rdi)
vmovntpd %ymm12, 0x180(%rdi)
vmovntpd %ymm13, 0x1A0(%rdi)
vmovntpd %ymm14, 0x1C0(%rdi)
vmovntpd %ymm15, 0x1E0(%rdi)
add $0x200, %rsi
add $0x200, %rdi
sub $0x200, %rdx
jnz avx_copy_unr15_loop
avx_copy_unr15_ret:
vzeroupper
ret
II. avx_copy_unr15_sw_prefetch
с предварительной выборкой ПО
Объявление:
void *avx_copy_unr15_sw_prefetch(void *restrict dest, const void *restrict src, size_t sz);
Реализация (согласно комментарию @Jester я изменил prefetcht0
для следующей итерации):
//rdi - dest
//rsi - src
//rdx - size
avx_copy_unr15_sw_prefetch:
cmp $0, %rdx
jz avx_copy_unr15_ret
avx_copy_unr15_loop:
prefetcht0 0x200(%rsi)
prefetcht0 0x240(%rsi)
prefetcht0 0x280(%rsi)
prefetcht0 0x2C0(%rsi)
prefetcht0 0x300(%rsi)
prefetcht0 0x340(%rsi)
prefetcht0 0x380(%rsi)
prefetcht0 0x3C0(%rsi)
vmovapd (%rsi), %ymm0
vmovapd 0x20(%rsi), %ymm1
vmovapd 0x40(%rsi), %ymm2
vmovapd 0x60(%rsi), %ymm3
vmovapd 0x80(%rsi), %ymm4
vmovapd 0xA0(%rsi), %ymm5
vmovapd 0xC0(%rsi), %ymm6
vmovapd 0xE0(%rsi), %ymm7
vmovapd 0x100(%rsi), %ymm8
vmovapd 0x120(%rsi), %ymm9
vmovapd 0x140(%rsi), %ymm10
vmovapd 0x160(%rsi), %ymm11
vmovapd 0x180(%rsi), %ymm12
vmovapd 0x1A0(%rsi), %ymm13
vmovapd 0x1C0(%rsi), %ymm14
vmovapd 0x1E0(%rsi), %ymm15
vmovntpd %ymm0, (%rdi)
vmovntpd %ymm1, 0x20(%rdi)
vmovntpd %ymm2, 0x40(%rdi)
vmovntpd %ymm3, 0x60(%rdi)
vmovntpd %ymm4, 0x80(%rdi)
vmovntpd %ymm5, 0xA0(%rdi)
vmovntpd %ymm6, 0xC0(%rdi)
vmovntpd %ymm7, 0xE0(%rdi)
vmovntpd %ymm8, 0x100(%rdi)
vmovntpd %ymm9, 0x120(%rdi)
vmovntpd %ymm10, 0x140(%rdi)
vmovntpd %ymm11, 0x160(%rdi)
vmovntpd %ymm12, 0x180(%rdi)
vmovntpd %ymm13, 0x1A0(%rdi)
vmovntpd %ymm14, 0x1C0(%rdi)
vmovntpd %ymm15, 0x1E0(%rdi)
add $0x200, %rsi
add $0x200, %rdi
sub $0x200, %rdx
jnz avx_copy_unr15_loop
avx_copy_unr15_ret:
vzeroupper
ret
Я измерил это, используя __rdtsc()
intrinsi c, доступный в x86intrin.h
, и примерно за 10 прогонов измерений я сделал около 20000 итераций и заметил, что предварительная выборка SW происходит примерно на 1
- 1.5
% быстрее.
Ошибка измерения или предварительная выборка ПО действительно помогает? Вот как выглядит одна итерация моего измерения:
#define DO_MEASURE(fn, iterations_total, ...) \
__extension__({\
uint64_t total_tsc = 0; \
unsigned total = iterations_total; \
while(total --> 0) { \
uint64_t start = __rdtsc(); \
fn(__VA_ARGS__); \
fn(__VA_ARGS__); \
fn(__VA_ARGS__); \
fn(__VA_ARGS__); \
fn(__VA_ARGS__); \
fn(__VA_ARGS__); \
fn(__VA_ARGS__); \
fn(__VA_ARGS__); \
uint64_t finish = __rdtsc(); \
uint64_t iteration_result = (finish - start) / 8; \
total_tsc += iteration_result; \
}; \
total_tsc / iterations_total; \
})
Вот как выглядит весь исходный файл для измерения:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include "memcp.h" //header with asm("") implementation
#include "benchmark.h" //header with DO_MEASURE macro shown above
#define BUF_SIZE 4096 * 1024
#define ITERATIONS 2000
_Alignas(4096) char src[BUF_SIZE];
_Alignas(4096) char dest[BUF_SIZE];
int main(void){
int fd = open("/dev/urandom", O_RDONLY);
read(fd, src, BUF_SIZE);
unsigned avx_unr15_iterations_total = 10;
unsigned avx_unr15_iteration = 0;
while(avx_unr15_iteration++ < avx_unr15_iterations_total){
unsigned long result = DO_MEASURE(avx_copy_unr15, ITERATIONS, dest, src, BUF_SIZE);
printf("avx_copy_unr15: iteration %u = %lu\n", avx_unr15_iteration, result);
}
unsigned avx_unr15_sw_prefetch_iterations_total = 10;
unsigned avx_unr15_sw_prefetch_iteration = 0;
while(avx_unr15_sw_prefetch_iteration++ < avx_unr15_sw_prefetch_iterations_total){
unsigned long result = DO_MEASURE(avx_copy_unr15_sw_prefetch, ITERATIONS, dest, src, BUF_SIZE);
printf("avx_copy_sw_prefetch_unr15: iteration %u = %lu\n", avx_unr15_sw_prefetch_iteration, result);
}
}
Компиляция с помощью -Werror -Wextra -pedantic -Wall -g3 -O3 -Wno-unused-result
Результат выполнения таких тестов таков:
avx_copy_unr15: iteration 1 = 296576
avx_copy_unr15: iteration 2 = 289416
avx_copy_unr15: iteration 3 = 287765
avx_copy_unr15: iteration 4 = 287584
avx_copy_unr15: iteration 5 = 287525
avx_copy_unr15: iteration 6 = 290479
avx_copy_unr15: iteration 7 = 286566
avx_copy_unr15: iteration 8 = 287082
avx_copy_unr15: iteration 9 = 287584
avx_copy_unr15: iteration 10 = 288825
avx_copy_sw_prefetch_unr15: iteration 1 = 288732
avx_copy_sw_prefetch_unr15: iteration 2 = 287374
avx_copy_sw_prefetch_unr15: iteration 3 = 286325
avx_copy_sw_prefetch_unr15: iteration 4 = 287374
avx_copy_sw_prefetch_unr15: iteration 5 = 297077
avx_copy_sw_prefetch_unr15: iteration 6 = 323929
avx_copy_sw_prefetch_unr15: iteration 7 = 321866
avx_copy_sw_prefetch_unr15: iteration 8 = 321503
avx_copy_sw_prefetch_unr15: iteration 9 = 321280
avx_copy_sw_prefetch_unr15: iteration 10 = 329869
Взятие perf record
на версию предварительной выборки SW показывает следующие результаты:
18,19 │ 0: prefetcht0 0x200(%rsi)
9,89 │ prefetcht0 0x240(%rsi)
8,85 │ prefetcht0 0x280(%rsi)
7,95 │ prefetcht0 0x2c0(%rsi)
7,02 │ prefetcht0 0x300(%rsi)
6,20 │ prefetcht0 0x340(%rsi)
5,58 │ prefetcht0 0x380(%rsi)
4,99 │ prefetcht0 0x3c0(%rsi)
1,42 │ vmovapd (%rsi),%ymm0
0,42 │ vmovapd 0x20(%rsi),%ymm1
1,44 │ vmovapd 0x40(%rsi),%ymm2
0,60 │ vmovapd 0x60(%rsi),%ymm3
1,22 │ vmovapd 0x80(%rsi),%ymm4
0,42 │ vmovapd 0xa0(%rsi),%ymm5
1,40 │ vmovapd 0xc0(%rsi),%ymm6
0,63 │ vmovapd 0xe0(%rsi),%ymm7
1,42 │ vmovapd 0x100(%rsi),%ymm8
0,47 │ vmovapd 0x120(%rsi),%ymm9
1,43 │ vmovapd 0x140(%rsi),%ymm10
0,61 │ vmovapd 0x160(%rsi),%ymm11
1,25 │ vmovapd 0x180(%rsi),%ymm12
0,47 │ vmovapd 0x1a0(%rsi),%ymm13
1,42 │ vmovapd 0x1c0(%rsi),%ymm14
0,67 │ vmovapd 0x1e0(%rsi),%ymm15
2,40 │ vmovntpd %ymm0,(%rdi)
0,95 │ vmovntpd %ymm1,0x20(%rdi)
0,79 │ vmovntpd %ymm2,0x40(%rdi)
0,90 │ vmovntpd %ymm3,0x60(%rdi)
0,77 │ vmovntpd %ymm4,0x80(%rdi)
0,85 │ vmovntpd %ymm5,0xa0(%rdi)
0,92 │ vmovntpd %ymm6,0xc0(%rdi)
1,00 │ vmovntpd %ymm7,0xe0(%rdi)
0,78 │ vmovntpd %ymm8,0x100(%rdi)
0,86 │ vmovntpd %ymm9,0x120(%rdi)
0,90 │ vmovntpd %ymm10,0x140(%rdi)
1,09 │ vmovntpd %ymm11,0x160(%rdi)
0,84 │ vmovntpd %ymm12,0x180(%rdi)
0,74 │ vmovntpd %ymm13,0x1a0(%rdi)
0,64 │ vmovntpd %ymm14,0x1c0(%rdi)
0,70 │ vmovntpd %ymm15,0x1e0(%rdi)