Мой вопрос основан на другом вопросе SO: Почему _mm_stream_ps создает пропуски кэша L1/LL?
Прочитав его и заинтриговав его, я попытался воспроизвести результаты и убедиться, что это было быстрее: наивный цикл, развернутый наивный цикл, _mm_stream_ps
(развернутый), _mm_store_ps
(разворачивается) и последний, но не наименьшее memset_pattern4
. (последний принимает 4 байтовый шаблон, такой как float, и пластыряет его по всему целевому массиву, который должен делать то же самое, что и все другие функции, но, вероятно, OS X exclusive).
Я позаботился о том, чтобы выровнять начало моего массива в кэше (64 байта, я проверил) и передать массив в аргументе, а также любые другие улучшения производительности, упомянутые в предыдущем вопросе.
Кто-то еще хотел узнать то же самое о гамедеве: http://www.gamedev.net/topic/532112-fast-memset/
Выводы этого потока отражают мои собственные: , когда целевой массив меньше самого большого (L3) кэша, _mm_store_ps
быстрее, чем _mm_stream_ps
. Когда целевой массив больше, _mm_stream_ps
быстрее. Я не совсем уверен, почему __mm_store_ps
работает быстрее в первом случае, так как я никогда не использую эти значения в кеше, но я понимаю, почему _mm_stream_ps
выигрывает в последнем случае. Это сделало для этой ситуации: напишите байты в память, которые вам не понадобятся сразу (или когда-либо).
Ниже приведены некоторые результаты с целевым массивом, в 256 раз превышающим кеш-память L3 (в моем случае 1,5 ГБ), скомпилированным с gcc 4.8:
gcc-4.8 stream.c -o stream -std=c11 -O3 -g3 -ftree-vectorize -march=native -minline-all-stringops && ./stream
bench L3-MASS, array 1610612736 bytes (402653184 floats, 0 remainder, 0x104803040 pointer)
warm up round...
6% ( 20.81148 ms) : MEMSET CHEAT
8% ( 28.49419 ms) : MEMSET PATTER
100% ( 371.40385 ms) : NAIVE NORMAL
54% ( 202.01147 ms) : NAIVE UNROLL
31% ( 113.53433 ms) : STREAM NORMAL
30% ( 111.41691 ms) : STREAM UNROLL
51% ( 190.70412 ms) : STORE NORMAL
51% ( 189.15338 ms) : STORE UNROLL
51% ( 189.36182 ms) : STORE PREFET
Итак, что мы узнаем из этого? memset_pattern4
невероятно быстро. Я включил bog-standard memset
, хотя он просто использует 1-байтовый шаблон для сравнения. По сути, читы memset
, но memset_pattern4
нет, и он по-прежнему злой быстро.
Я пробовал посмотреть сборку на то, что, по моему мнению, является исходным кодом для memset_pattern4
в строковой библиотеке OS X:
- Apple libc,
memset_pattern4
: http://www.opensource.apple.com/source/Libc/Libc-825.25/string/memset_pattern.c?txt - Это ссылается на так называемую функцию bcopy. Позвольте копать для этого: Строковая библиотека: http://www.opensource.apple.com/source/Libc/Libc-763.13/x86_64/string/
- И, скорее всего, версия SSE 4.2 используется в моем случае: http://www.opensource.apple.com/source/Libc/Libc-763.13/x86_64/string/bcopy_sse42.s
Мое знание asm достигает (к настоящему времени) достаточно далеко, что я вижу, что они используют инструкцию movdqa
, где это имеет значение (в разделе LAlignedLoop
), который в основном представляет собой инструкцию перемещения SSE для целых чисел (не плавает), внутренняя: _mm_store_si128
. Не то, чтобы это имело значение здесь, биты и байты, правильно?
- Также существует чистая реализация asm
memset_pattern4
, которая кажется иной, поскольку она не вызываетbcopy
: http://www.opensource.apple.com/source/Libc/Libc-763.13/x86_64/string/memset.s ( РЕДАКТИРОВАТЬ: это правильный, как проверено при запуске под gdb)
... черт, этот, кажется, использует невременные (_mm_stream_ps
магазины для очень длинных массивов = > movntdq %xmm0,(%rdi,%rcx)...
, посмотрите в разделе LVeryLong
funcion), что я и делаю! Так как это может быть намного быстрее? Возможно, это не те memset_pattern4
, которые я ищу.
Итак, что memset_pattern4
делает под капотом и почему он в 5 раз быстрее, чем моя лучшая попытка?. Хотя я пытался узнать достаточно сборки x86, чтобы иметь возможность анализировать Я боюсь, что это немного из моей лиги, чтобы отлаживать проблемы с производительностью в оптимизированных до смерти функциях.
ПРИМЕЧАНИЕ: для тех, кто любопытен, этот микрообъект также служит для иллюстрации явной удивительности clang и его расширенной векторизации (-fslp-vectorize
), ему удается сделать наивный цикл наиболее быстрым, сохраняя memset почти во всех случаях. Кажется, что это примерно так же хорошо, как лучшая комбинация _mm_store_ps
и _mm_stream_ps
.
CODE: здесь код, который я использую для выполнения моего теста (в виде gist: https://gist.github.com/6571379):
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <assert.h>
/**
* compile and run:
*
* OSX:
* clang stream.c -o stream -std=c11 -O3 -g -ftree-vectorize -fslp-vectorize -march=native -minline-all-stringops && ./stream
* gcc-4.8 stream.c -o stream -std=c11 -O3 -g3 -ftree-vectorize -march=native -minline-all-stringops && ./stream
*
* linux:
* clang stream.c -o stream -lrt -std=c11 -O3 -ftree-vectorize -fslp-vectorize -march=native && ./stream
* gcc-4.8 stream.c -o stream -lrt -std=c11 -O3 -ftree-vectorize -march=native && ./stream
*
* to generate the assembly:
* gcc-4.8 -S stream.c -o stream.s -std=c11 -O3 -g3 -ftree-vectorize -march=native -minline-all-stringops
* gobjdump -dS stream > stream.obj.s
*
* clang is the (very clear) winner here, the SLP vectorizer is absolutely killer, it even turns the
* plain naive loop into something hyper-performant
*/
/* posix headers */
#include <sys/time.h>
/* intrinsics */
#include <x86intrin.h>
#define ARRAY_SIZE(x) ((sizeof(x)/sizeof(0[x])) / ((size_t)(!(sizeof(x) % sizeof(0[x])))))
/**
* some stats from my system
*
* sudo sysctl -a | grep cache
*
* hw.cachelinesize = 64
* hw.l1icachesize = 32768
* hw.l1dcachesize = 32768
* hw.l2cachesize = 262144
* hw.l3cachesize = 6291456
*/
/* most processors these days (2013) have a 64 byte cache line */
#define FACTOR 1024
#define CACHE_LINE 64
#define FLOATS_PER_LINE (CACHE_LINE / sizeof(float))
#define L1_CACHE_BYTES 32768
#define L2_CACHE_BYTES 262144
#define L3_CACHE_BYTES 6291456
#ifdef __MACH__
#include <mach/mach_time.h>
double ns_conversion_factor;
double us_conversion_factor;
double ms_conversion_factor;
void timeinit() {
mach_timebase_info_data_t timebase;
mach_timebase_info(&timebase);
ns_conversion_factor = (double)timebase.numer / (double)timebase.denom;
us_conversion_factor = (double)timebase.numer / (double)timebase.denom / 1000;
ms_conversion_factor = (double)timebase.numer / (double)timebase.denom / 1000000;
}
double nsticks() {
return mach_absolute_time() * ns_conversion_factor;
}
double msticks() {
return mach_absolute_time() * ms_conversion_factor;
}
#else
void timeinit() {
/* do nothing */
}
double nsticks() {
timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ((double)ts.tv_sec) / 1000000000 + ((double)ts.tv_nsec);
}
double msticks() {
timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ((double)ts.tv_sec) / 1000 + ((double)ts.tv_nsec) * 1000000;
}
#endif
void *aligned_malloc(size_t size, size_t alignment) {
void *pa, *ptr;
pa = malloc((size+alignment-1)+sizeof(void *));
if (!pa) return NULL;
ptr=(void*)( ((intptr_t)pa+sizeof(void *)+alignment-1)&~(alignment-1) );
*((void **)ptr-1)=pa;
return ptr;
}
void aligned_free(void *ptr) {
if (ptr) free(*((void **)ptr-1));
}
void pollute_cache(uint8_t volatile *arr, size_t length) {
for (int i = 0; i < length; ++i) {
arr[i] = (arr[i] > 0xFE) ? 0xAA : 0x55;
}
}
void pollute_cache_standalone() {
const size_t pollute_len = 2 * L3_CACHE_BYTES;
uint8_t *arr = aligned_malloc(pollute_len * sizeof(uint8_t), 64);
for (int i = 0; i < pollute_len; ++i) {
arr[i] = (arr[i] > 0xFE) ? 0xAA : 0x55;
}
aligned_free(arr);
}
/**
* returns the time passed, in milliseconds
*/
double tim(const char *name, double baseline, void (*pre)(void), void (*func)(float *, size_t), float * restrict arr, size_t length) {
struct timeval t1, t2;
if (pre) pre();
const double ms1 = msticks();
func(arr, length);
const double ms2 = msticks();
const double ms = (ms2 - ms1);
if (baseline == -2.0) return ms;
/* first run, equal to baseline (itself) by definition */
if (baseline == -1.0) baseline = ms;
if (baseline != 0.0) {
fprintf(stderr, "%7.0f%% (%10.5f ms) : %s\n", (ms / baseline) * 100, ms, name);
}
else {
fprintf(stderr, "%7.3f ms : %s\n", ms, name);
}
return ms;
}
void func0(float * const restrict arr, size_t length) {
memset(arr, 0x05, length);
}
#ifdef __MACH__
void funcB(float * const restrict arr, size_t length) {
const float val = 5.0f;
memset_pattern4(arr, &val,length);
}
#endif
void func1(float * const restrict arr, size_t length) {
for (int i = 0; i < length; ++i) {
arr[i] = 5.0f;
}
}
void func2(float * const restrict arr, size_t length) {
for(int i = 0; i < length; i += 4) {
arr[i] = 5.0f;
arr[i+1] = 5.0f;
arr[i+2] = 5.0f;
arr[i+3] = 5.0f;
}
}
void func3(float * const restrict arr, size_t length) {
const __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 4) {
_mm_stream_ps(&arr[i], buf);
}
_mm_mfence();
}
void func4(float * const restrict arr, size_t length) {
const __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 16) {
_mm_stream_ps(&arr[i + 0], buf);
_mm_stream_ps(&arr[i + 4], buf);
_mm_stream_ps(&arr[i + 8], buf);
_mm_stream_ps(&arr[i + 12], buf);
}
_mm_mfence();
}
void func5(float * const restrict arr, size_t length) {
const __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 4) {
_mm_store_ps(&arr[i], buf);
}
}
void fstore_prefetch(float * const restrict arr, size_t length) {
const __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 16) {
__builtin_prefetch(&arr[i + FLOATS_PER_LINE * 32], 1, 0);
_mm_store_ps(&arr[i + 0], buf);
_mm_store_ps(&arr[i + 4], buf);
_mm_store_ps(&arr[i + 8], buf);
_mm_store_ps(&arr[i + 12], buf);
}
}
void func6(float * const restrict arr, size_t length) {
const __m128 buf = _mm_setr_ps(5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 16) {
_mm_store_ps(&arr[i + 0], buf);
_mm_store_ps(&arr[i + 4], buf);
_mm_store_ps(&arr[i + 8], buf);
_mm_store_ps(&arr[i + 12], buf);
}
}
#ifdef __AVX__
void func7(float * restrict arr, size_t length) {
const __m256 buf = _mm256_setr_ps(5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 8) {
_mm256_stream_ps(&arr[i], buf);
}
}
void func8(float * restrict arr, size_t length) {
const __m256 buf = _mm256_setr_ps(5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 32) {
_mm256_stream_ps(&arr[i + 0], buf);
_mm256_stream_ps(&arr[i + 8], buf);
_mm256_stream_ps(&arr[i + 16], buf);
_mm256_stream_ps(&arr[i + 24], buf);
}
}
void func9(float * restrict arr, size_t length) {
const __m256 buf = _mm256_setr_ps(5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 8) {
_mm256_store_ps(&arr[i], buf);
}
}
void funcA(float * restrict arr, size_t length) {
const __m256 buf = _mm256_setr_ps(5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f, 5.0f);
for (int i = 0; i < length; i += 32) {
_mm256_store_ps(&arr[i + 0], buf);
_mm256_store_ps(&arr[i + 8], buf);
_mm256_store_ps(&arr[i + 16], buf);
_mm256_store_ps(&arr[i + 24], buf);
}
}
#endif
void bench(const char * restrict name, float * restrict arr, size_t length) {
fprintf(stderr, "bench %s, array %zu bytes (%zu floats, %zu remainder, %p pointer)\n", name, length, length / sizeof(float), length % sizeof(float), arr);
size_t nfloats = length / sizeof(float);
fprintf(stderr, "warm up round...");
func1(arr, nfloats);
fprintf(stderr, "done\n");
double baseline = tim("func1: NAIVE ", -2.0, NULL, func1, arr, nfloats);
tim("MEMSET CHEAT ", baseline, NULL, func0, arr, nfloats);
#ifdef __MACH__
tim("MEMSET PATTER", baseline, NULL, funcB, arr, nfloats);
#endif
tim("NAIVE NORMAL", -1.0, NULL, func1, arr, nfloats);
tim("NAIVE UNROLL", baseline, NULL, func2, arr, nfloats);
tim("STREAM NORMAL", baseline, NULL, func3, arr, nfloats);
tim("STREAM UNROLL", baseline, NULL, func4, arr, nfloats);
tim("STORE NORMAL", baseline, NULL, func5, arr, nfloats);
tim("STORE UNROLL", baseline, NULL, func6, arr, nfloats);
tim("STORE PREFET", baseline, NULL, fstore_prefetch, arr, nfloats);
// for (int i = 0; i < 1; ++i) {
// tim("func0: MEMSET (cache polluted)", NULL, func0, arr, nfloats);
// tim("func1: NAIVE (cache polluted)", pollute_cache_standalone, func1, arr, nfloats);
// tim("func2: UNROLL (cache polluted)", pollute_cache_standalone, func2, arr, nfloats);
// tim("func3: STREAM (cache polluted)", pollute_cache_standalone, func3, arr, nfloats);
// tim("func4: STRUN (cache polluted)", pollute_cache_standalone, func4, arr, nfloats);
// tim("func5: STORE (cache polluted)", pollute_cache_standalone, func5, arr, nfloats);
// tim("func6: STOUN (cache polluted)", pollute_cache_standalone, func6, arr, nfloats);
// }
}
int main() {
timeinit();
static const struct {
const char *name;
size_t bytes;
} sizes[] = {
{ "L1-HALF", L1_CACHE_BYTES / 2 },
{ "L1-FULL", L1_CACHE_BYTES },
{ "L2-HALF", L2_CACHE_BYTES / 2 },
{ "L2-FULL", L2_CACHE_BYTES },
{ "L3-HALF", L3_CACHE_BYTES / 2 },
{ "L3-FULL", L3_CACHE_BYTES },
{ "L3-DOUB", L3_CACHE_BYTES * 2 },
{ "L3-HUGE", L3_CACHE_BYTES * 64 },
{ "L3-MASS", L3_CACHE_BYTES * 256 }
};
for (int i = 0; i < ARRAY_SIZE(sizes); ++i) {
size_t bytes = sizes[i].bytes;
/* align to cache line */
float *arr = aligned_malloc(bytes, CACHE_LINE);
bench(sizes[i].name, arr, bytes);
aligned_free(arr);
}
return 0;
}
РЕДАКТИРОВАТЬ. Я пошел копать немного дальше и после редактирования сборки, которую генерирует gcc, чтобы сделать ее более или менее той же, что и одно яблоко (memset.s
, label LVeryLong
, т.е.: 4 развернутых movntdq
инструкций в узком цикле). К моему удивлению, я получаю равную производительность, как и мои функции, которые используют _mm_store_ps
(movaps
). Это меня озадачивает, так как я ожидал, что это будет
- с точностью
memset_pattern4
(предположительно разворачиваетсяmovntdq
) - так быстро, как разворачивается
_mm_stream_ps
(movntdq
)
Но нет, это похоже на _mm_store_ps
, представьте, что, может быть, я делаю что-то неправильно. Запуск objdump в полученном двоичном файле подтверждает, что он использует movntdq
, что еще больше удивляет меня, что, черт возьми, происходит?
Потому что я наткнулся на тупик, я решил пройти через исполняемый файл в отладчике и установить точку останова в memset_pattern4
. Вступив в функцию, я заметил, что он делает именно то, что я думал, что это будет делать, жесткий цикл с четырьмя развернутыми movntdq
:
0x00007fff92a5f7d2 <+318>: jmp 0x7fff92a5f7e0 <memset_pattern4+332>
0x00007fff92a5f7d4 <+320>: nopw 0x0(%rax,%rax,1)
0x00007fff92a5f7da <+326>: nopw 0x0(%rax,%rax,1)
0x00007fff92a5f7e0 <+332>: movntdq %xmm0,(%rdi,%rcx,1)
0x00007fff92a5f7e5 <+337>: movntdq %xmm0,0x10(%rdi,%rcx,1)
0x00007fff92a5f7eb <+343>: movntdq %xmm0,0x20(%rdi,%rcx,1)
0x00007fff92a5f7f1 <+349>: movntdq %xmm0,0x30(%rdi,%rcx,1)
0x00007fff92a5f7f7 <+355>: add $0x40,%rcx
=> 0x00007fff92a5f7fb <+359>: jne 0x7fff92a5f7e0 <memset_pattern4+332>
0x00007fff92a5f7fd <+361>: sfence
Итак, что делает яблочный соус гораздо более волшебным, чем мой, интересно...
РЕДАКТИРОВАТЬ 2: я дважды ошибался здесь, яблочный волшебный соус не настолько волшебный, я просто проходил массив, который был в 4 раза меньше того, что я передавал своим функциям. Предоставлено @PaulR для заметок! Во-вторых, я редактировал сборку функции, но gcc уже вложил ее в нее. Поэтому я редактировал копию, которая никогда не использовалась.
ЗАКЛЮЧЕНИЕ:
Некоторые другие вещи, которые я узнал:
- Clang и gcc действительно хороши, с правильными внутренними функциями, они много оптимизируют (и clang даже делает отличную работу без встроенных функций, когда включен вектор-вектор SLP). Они также будут встроены указатели на функции.
- Clang заменит наивный цикл константой на вызов
memset
, очистив другой запутанный результат, который у меня был. - невременное хранилище (т.е.: поток) полезно только при огромной записи
-
memset
действительно хорошо оптимизирован, он автоматически переключается между обычным хранилищем и невременным хранилищем (потоком) на основе длины массива для записи. Я не уверен, насколько это верно на платформах, отличных от OSX. - при написании теста убедитесь, что функция делает то, что вы думаете, и что компилятор не перехитрил вас. Первый случай был моей проблемой здесь, я не дал правильные аргументы.
EDIT. Недавно я наткнулся на руководство по оптимизации intel, если это вообще интересует эти вещи, прочитайте некоторые части этого первого (начинаются с 3.7.6, возможно).