Speedup memcpy

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Всем привет. Мне интересно, насколько можно ускорить memcpy.
Вот код

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <chrono>

void memcpy_MG(void* dst, void* src, size_t sz)
{
	uint8_t* __restrict__ dst8 = (uint8_t*) __builtin_assume_aligned(dst, 64);
	uint8_t* __restrict__ src8 = (uint8_t*) __builtin_assume_aligned(src, 64);

	for(size_t it = 0; it < sz; it++)
	{
		dst8[it] = src8[it];
	};
}

int main(int argc, char** argv)
{
	(void)argc;
	(void)argv;
	std::chrono::duration<double> elapsed;
	size_t sz = 1920*1080*3;
	void* src = NULL;
	posix_memalign(&src, 64, sz);
	void* dst = NULL;
	posix_memalign(&dst, 64, sz);

	auto t0 = std::chrono::high_resolution_clock::now();
	memcpy_MG(dst, src, sz);
	auto t1 = std::chrono::high_resolution_clock::now();
	elapsed = (t1 - t0);
	std::cout << "memcpy_MG time: " << sz/elapsed.count()/1024/1024/1024 << std::endl;

	t0 = std::chrono::high_resolution_clock::now();
	memcpy_MG(dst, src, sz);
	t1 = std::chrono::high_resolution_clock::now();
	elapsed = (t1 - t0);
	std::cout << "memcpy_MG time: " << sz/elapsed.count()/1024/1024/1024 << std::endl;

	t0 = std::chrono::high_resolution_clock::now();
	for(int i=0; i<10; i++)
		memcpy_MG(dst, src, sz);
	t1 = std::chrono::high_resolution_clock::now();
	elapsed = (t1 - t0)/10;
	std::cout << "memcpy_MG time: " << sz/elapsed.count()/1024/1024/1024 << std::endl;

	t0 = std::chrono::high_resolution_clock::now();
	for(int i=0; i<10; i++)
		memcpy(dst, src, sz);
	t1 = std::chrono::high_resolution_clock::now();
	elapsed = (t1 - t0)/10;
	std::cout << "memcpy time: " << sz/elapsed.count()/1024/1024/1024 << std::endl;

	return 0;
}

memcpy_MG — Это совет Майка.

Получаются след результаты в гигабайтах в сек:

memcpy_MG time: 1.46773
memcpy_MG time: 5.80593
memcpy_MG time: 7.10735
memcpy time: 6.79568

Юзал Cmakе со след ключами:
set(CMAKE_CXX_FLAGS «-std=c++11 -Wall -Wextra -Wno-unused-local-typedefs -Wno-unused-result-O3 -fno-builtin -msse4»)

По сути вопросы. Можно-ли еще быстрее memcpy сделать?
И можно-ли как-то ускорить именно первый вызов?
AVX2 у меня нет.

А вот такой код

	void* s[10];
	for(size_t i=0; i<10; ++i)
		posix_memalign(&s[i], 64, sz);
	void* d[10];
	for(size_t i=0; i<10; ++i)
		posix_memalign(&d[i], 64, sz);
	t0 = std::chrono::high_resolution_clock::now();
	for(int i=0; i<10; i++)
		memcpy_MG(d[i], s[i], sz);
	t1 = std::chrono::high_resolution_clock::now();
	elapsed = (t1 - t0)/10;
	std::cout << "sec memcpy_MG time: " << sz/elapsed.count()/1024/1024/1024 << std::endl;

Дает уже

sec memcpy_MG time: 1.45273
👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

посмотреть имплементацию ffmpeg?

Вот ещё одна идея для оптимизации для больших данных, которую мы реально применяем — это page swizzling и memory interleaving. В зависимости от настроек контроллера памяти мы может иметь двух-трёх-шести канальную память. Обычно двухканальная, если это не high end железка.

Если не вдаваться в детали организации памяти, то довольно успешно применяется два подхода:

1) Наивный — делим память пополам и в два потока копируем две половинки.
2) Псевдонаучный — два потока копируют один чётные страницы по 4096 байт, второй нечётные.

Врядли мы попадём в аппаратные настройки контроллеров памяти, но даже тот факт, что уменьшить процент простоя контроллера памяти можно до 50-70% с таким применением, уже говорит о том, что такой подход стоит рассмотреть.

Если вникать в детали настроек контроллера памяти, то в случае одной интелловской платформы мы пытались разнести обращение к памяти по разным контроллерам, в которой интерливинг был 512 байт на каждый из контроллеров.

Единственное, что на GP OS скорей всего придётся делать ручной спин потокам перед копированием. Хотя на больших данных этим можно пренебречь.

Как вы тему писать начали я сразу же ж задумался и наконец руки дошли получить цифры.

На собранном на винде под mingw memcpy() по блокам full hd 32 бит на время работы 30 секунд (случайно подобралось потом подтюнил «для плавности статистики») выходит примерно 5,2 ГБ/с в однопоточном режиме.

В многопоточном режиме (точнее в многопроцессном полагаю можно считать равным в пределах погрешности причём с более высокой достоверностью) выходит 5,3-5,4 ГБ/с и дальше никак.

Память физически стоит в 2-канальном режиме.

ЗЫ: что занимательно на самом деле это результаты memmove() а не memcpy() как ни странно первый таки быстрее хотя и совсем ненамного но таки да.

Upd: но я решил попробовать и сделал то же ж самой «ближе к нейтив платформе» и собрал на vcpp в однопоточном несомненно помогло 7,75 ГБ/с но в многопоточном «остался при своём» тех же ж 7,75 ГБ/с в сумме.

Смотри, тут такое дело, нужно заглянуть под капот и посмотреть сколько ALU и SIMD блоков в процессоре, отсюда и плясать. Для прототипирования я использовал openmp:

g++ mcpy.cpp -O3 -msse4 -march=core2 -fno-builtin -std=c++11 -ftree-vectorize -fopt-info-vec -fno-strict-aliasing -fopenmp -o mcpy

Код из статьи оригинальный, только количество итераций я сделал не 10, а 10000, чтобы убрать шум.

void memcpy_MG(void* __restrict__ dst, void* __restrict__ src)
{
        uint64_t* __restrict__ dst64 = (uint64_t*) __builtin_assume_aligned(dst, 16);
        uint64_t* __restrict__ src64 = (uint64_t*) __builtin_assume_aligned(src, 16);

        #pragma omp parallel for
        for(size_t jt = 0; jt < 1920 * 1080 * 3 / 128; jt++)
        {
                for(size_t it = 0; it < 16; it++)
                {
                        dst64[jt * 16 + it] = src64[jt * 16 + it];
                }
        }
}

Без опции -fopenmp:

root@mikenfs:~# nice -n+20 ./mcpy
memcpy_MG time: 1.47402
memcpy_MG time: 8.18321
memcpy_MG time: 10.7871
memcpy time: 10.5639

С опцией -fopenmp
root@mikenfs:~# nice -n+20 ./mcpy
memcpy_MG time: 0.262575
memcpy_MG time: 9.27651
memcpy_MG time: 14.6497
memcpy time: 10.9807

Первым, что бросилось в глаза — автовекторизация оказалась сломанной. -fopenmp-simd — убрал нафиг всю многопоточность и оставил только SIMD операции.

void memcpy_MG(void* __restrict__ dst, void* __restrict__ src)
{
        __m128* __restrict__ dst128 = (__m128*) __builtin_assume_aligned(dst, 16);
        __m128* __restrict__ src128 = (__m128*) __builtin_assume_aligned(src, 16);

        #pragma omp parallel for
        for(size_t jt = 0; jt < 1920 * 1080 * 3 / 128; jt++)
        {
                for(size_t it = 0; it < 8; it++)
                {
                        dst128[jt * 8 + it] = src128[jt * 8 + it];
                }
        }
}

даёт практически те же результаты, может на +0.2 быстрее.

Убрал HyperThread ядра с «#pragma omp parallel for num_threads(2)»
и получил:

root@mikenfs:~# nice -n+20 ./mcpy
memcpy_MG time: 2.35501
memcpy_MG time: 11.9389
memcpy_MG time: 15.0848
memcpy time: 10.9468

Почти в 1.4 раза многопоточная реализация быстрее.

Если я правильно считаю 1920 * 1080 * 3 по 10 тыс. раз у тебя получается 57,94 ГБ данных и соотв. время «memcpy time: 10.9468» выглядит похоже на мои результаты и даёт 5,29 ГБ/с а что такое у тебя «memcpy_MG time:» ?

Кстати интересный вопрос кто крут в теории DDR? )) теоретический bandwidth 21 ГБ/с (для условных ddr3 1333) это «дуплекс» и соотв. предел копирования из памяти в память (соотв. сперва чтение затем запись) таки те же ж 21 ГБ/с или же ж «полудуплекс» и соотв. в случае копирования из памяти в память общий bandwidth надо делить на 2?

ЗЫ: но я попробовал memset() даёт уже 22-23 ГБ/с и тоже не параллелится 2 потока дают по 10,25 что даже меньше одного. Цифры уже подобны теоретической пропускной способности полагаю считать их достоверными.

а что такое у тебя «memcpy_MG time:» ?

Статья в топике, я от туда код тянул без изменений, кроме количества итераций.

теоретический bandwidth 21 ГБ/с (для условных ddr3 1333) это «дуплекс» и соотв. предел копирования из памяти в память (соотв. сперва чтение затем запись) таки те же ж 21 ГБ/с или же ж «полудуплекс» и соотв. в случае копирования из памяти в память общий bandwidth надо делить на 2?

Для DDR3 1333 это 10.666Gb/s чистого чтения или чистой записи. Если ты цифру утащил с сайта интела, то они её умножили на два, т.к. у них два контроллера памяти и они могут поддерживать двух канальную память.

Я тестировал на десятилетнем Intel® Core™ i7 860, вот только у него кеш размером 8Мб, что соизмеримо с размером фрема. Как только размер блока был увеличен до 16Мб, то всё сразу стухло почти в два раза:

memcpy_MG time: 1.90208
memcpy_MG time: 7.04582
memcpy_MG time: 7.6618
memcpy time: 6.50226

Занимательно. В таком случае для i7 860 (максимальная пропускная памяти 21 ГБ/с) цифры для копирования память-память мне кажутся странными...

memcpy_MG time: 14.6497
memcpy_MG time: 11.9389
memcpy_MG time: 15.0848

Даже цифра memcpy() чуть великовата но уже по крайней мере «в рамках теории».

memcpy time: 10.9468

Если подумать... я так понимаю в «особо оптимальных случаях» может заполняться кеш и оставаться валидным и не требовать доп. чтения из памяти и соотв. реального копирования «память-память» не происходит а происходит «размножение» одного и того же ж «кадра».

ЗЫ: я увеличил размер кадра х2 и получил те же ж цифры 7,74 ГБ/с в однопоточном а вот в 2-поточном уже чуть больше 7,81 ГБ/с в сумме.

и не требовать доп. чтения из памяти и соотв. реального копирования «память-память» не происходит

Так и есть, причём не только чтения, а ещё и записи. Ну и на то он и кеш, что память будет обновлена только при случае или по команде. Можно использовать непривелигированную команду: clflush/clflushopt с инкрементом адреса по 64 байта и посбрасывать кеш принудительно в память для всех отложенных операций.

void clflush(volatile void *__p)
{
asm volatile("clflush %0″ : «+m» (*(volatile char*)__p));
}

Ну ок это хорошо что я всё ещё способен объяснять техническое происхождение теоретически нереальных результатов эмпирических. ))

Врядли мы попадём в аппаратные настройки контроллеров памяти, но даже тот факт, что уменьшить процент простоя контроллера памяти можно до 50-70% с таким применением, уже говорит о том, что такой подход стоит рассмотреть.

Я можу помилятися, але мені здавалося, що процесор все одне віртуалізує всі адреси під капотом, тобто реальна адреса може ніколи не співпадати із тією, що фігурує на рівні програми. То може статися, що навіть при розділенні пам’яті на дві половинки на рівні коду, в реальності все одне буде записано на одну планку пам’яті в сусідні банки. Якщо в сервері стоїть LRDIMM, то там взагалі може важко буде попасти на сусідні канали, бо не кожна програма буде виділяти, наприклад, 32GB пам’яті за раз. Нагадую, що LRDIMM дозволяє зараз 128G на планку набирати.

що процесор все одне віртуалізує всі адреси під капотом, тобто реальна адреса може ніколи не співпадати із тією, що фігурує на рівні програми.

В рамках одной страницы совпадает всегда.

То може статися, що навіть при розділенні пам’яті на дві половинки на рівні коду, в реальності все одне буде записано на одну планку пам’яті в сусідні банки.

Такого никогда не будет, ибо смысла в многоканальном контроллере тогда никакого нет. Основная идея разбития блока банально на два разных куска — это просто возможность рассинхронизировать обращение к памяти. При одновременном обращении к одному физическому каналу второй процессор будет просто стоять до тех пор пока первый опытным путём не дойдёт до второго канала и снимет блокировку с первого, после они будут молотить приблизительно паралелльно, но всегда будут попадать на участки с одним контроллером, поэтому 100 процентов достичь без знания реального лейаута невозможно, но улучшение вызванное параллельным доступом всё равно будет.

кщо в сервері стоїть LRDIMM, то там взагалі може важко буде попасти на сусідні канали, бо не кожна програма буде виділяти, наприклад, 32GB пам’яті за раз. Нагадую, що LRDIMM дозволяє зараз 128G на планку набирати.

Процессор уже лет 20 не читает напрямую с памяти. На себя это берёт контроллер памяти. Обычно типичная конфигурация 4-8 кешлайнов на канал, итого каналы памяти чередуются каждые 256-512 байт, что позволяет обычному десктопному процессору быть быстрее без особых оптимизаций, но не всегда.

Лет этак доифга назад, один наш дев переписал memcpy на SSE2, не помню насколько но ускорилось, по сравненю со стандартным из glibc.

Гуглил?
советую начать вот отсюда
www.codeproject.com/...​memcpy-memmove-on-x-x-EVE

На самом деле она далеко не самая быстрая. Просто многие почему-то не понимают, что memcpy — это не вещь в себе, её нельзя рассматривать в отрыве от остального кода. Их реализация просто уничтожает кеш в прямом смысле слова и засырает его кусками копируемой памяти, при том, что вероятность повторного использования приближается к 0. После такого memcpy процессор будет заново забивать кеш часто-используемыми кусками памяти, что приведёт к ещё большим тормозам, ну что, что операция копирования прошла быстро, для бенчмарка отлично, только зато всё остальное теперь медленно.

В SSE 4.1 ввели non-temporal instructions: www.felixcloutier.com/x86/MOVNTDQA.html , как раз, чтобы уменьшить cache pollution. Например на ApolloLake Atom их операция копирования получает скорость порядка 5Gb/s с полностью уничтоженным 2Mb cache, non-temporal пролучает 4.7Gb/s со всеми сохранёнными кешами, что на общей производительности сказывается фантастически.

Може спробувати позбавитись окремого лічильника? Та змінити тип на uint64_t

void memcpy_MG(void* dst, void* src, size_t sz)
{
	uint64_t* __restrict__ dst64 = (uint64_t*) __builtin_assume_aligned(dst, 64);
	uint64_t* __restrict__ src64 = (uint64_t*) __builtin_assume_aligned(src, 64);

	uint64_t* __restrict__ end64 = src64 + (sz >> 3);

	while (src64 != end64)
	{
		*src++ = *dst++;
	};
}

Умножение путем сдвига выполняется по религиозной причине, или в данном случае в этом есть какой-то смысл?

Для тех, кто любит компилить дебажную версию с -О0 есть.

Да ну? И в чем смысл?
Скрин: piccy.info/...​a9da83d6891aa504bc6/orig

Забавно то, что в «оптимизированном» примере — деление, а с делением действительно отработает быстрее в 64 раза, тут не поспоришь.

Да ну? И в чем смысл?
int f(int x)
{
    return x >> 3;
}
<------>pushl<->%ebp
<------>movl<-->%esp, %ebp
<------>movl<-->8(%ebp), %eax
<------>sarl<-->$3, %eax
<------>popl<-->%ebp

vs.

int f(int x)
{
    return x / 8;
}
<------>pushl<->%ebp
<------>movl<-->%esp, %ebp
<------>movl<-->8(%ebp), %eax
<------>leal<-->7(%eax), %edx
<------>testl<->%eax, %eax
<------>cmovs<->%edx, %eax
<------>sarl<-->$3, %eax
<------>popl<-->%ebp

но мы ведь про умножение на 8 говорили, а не как написать максимально быстрый некорректный код, да?

А где там умножение?

uint64_t* __restrict__ end64 = src64 + (sz >> 3);

Мы говорили про это.

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

Это оптимизация по результатам профайлинга? Если да, то на сколько ускоряет код?
Если нет — то какого, собственно, мотива, мы оптимизируем с ухудшением читаемости кода до того, как убедились, что это улучшит результат?

Это оптимизация по результатам профайлинга? Если да, то на сколько ускоряет код?

Какой профайлинг с -О0? %)

такой же, как с -Ofast, но с -O0. Только в чтении проще. Или мы больше не оптимизируем дебажный код?

Лично я последние несколько лет не компилю дебажный код, поэтому мне всё равно оно там оптимизированное или нет %) Такая специфика, что тайминги в GPU измеряются в микросекундах и если дебажный код их увеличивает, то часто получается, что в дебаг режиме мы отлаживаем одну ветку кода, а в релизе совершенно другое. Например, нормальное дело когда 200-400 наносекунд мы делаем поллинг аппаратуры без CPU relax, 1-2 микросекунды с CPU relax а ля asm volatile("pause\n": : :"memory"); и затем тупо ожидаем прерывания от железки. 30-40% всех реквестов GPU выполняет в первом случае, процентов 25% во втором и всё остальное в третьем, в дебажном режиме мы первый кейс может вообще тупо прозевать. А чем меньше время простоя, тем выше общая загрузка и скорость работы.

Умножение путем сдвига выполняется по религиозной причине

У славу Сотоні, звичайно!

По поводу всех этих замеров вспоминается прикольная статейка:
cis.upenn.edu/...​/producing-wrong-data.pdf

memcpy_MG time: 1.46773
Дает уже
sec memcpy_MG time: 1.45273

Мой поинт в том, что разница в скорости в ~1% может быть вызвана чем угодно. Примеры «чего угодно» можно глянуть в статье.

Могу сразу сказать, что при работе с одномерными сигналами обращаешь внимание совсем на другое, в отличие от двумерных. Там совсем другие нюансы (хоть что-то отдаленно похожее тоже есть). Там тоже фрэймы, но во-первых одномерные, во-вторых маленькие, в сравнении с видео. И да часто там выгодно те фрэймы в виде матрицы собрать и обработать разом, чтобы быстрее считало, но там все одно или 1D или ND на верхних уровнях (где уже ML).

Не все так просто, там нюансов просто вагон и маленькая тележка. Вот офигенный курс по архитектуре CPU : courses.cs.washington.edu/...​548/12au/video/index.html
по нему ровным слоем размазаны оптимизации: бранч предикшен, реордеринг, кэш алаймент, кэш когерентность и прочее. Я бы не стал утверждать одно или другое не видя что делает код и где он ранится.

Вот пара примеров на подумать:
— Если код малтитредед, то есть шанс фолс шэринга при хранении в виде матрицы.
— Если код сингл тредед, то возможно имеет смысл отключить гипертрединг, чтобы максимально загрузить реордер буффер и пайплайны.
— Я бы еще подкрутил процесс приорити до максимума, чтобы избежать лишних контекст свитчей, это не сделает код быстрее, но сделает замеры более точными.
— Можно открыть сгенереный код в IDA и посмотреть во что превратился цикл, достаточно годный компилятор мог бы развернуть цикл(на сколько возможно) или добавить мультискалярные инструкции.

Я бы не стал утверждать одно или другое не видя что делает код и где он ранится.

Я имею ввиду код который вызывает memcpy в продакшн системе.

Если код малтитредед, то есть шанс фолс шэринга при хранении в виде матрицы.

Тут 2 крайности: максимально выровнять по размеру кэш лайна, тогда можно минимизировать фолс шэринг, но в кэш будет влазить меньше данных или наоборот.

Если код сингл тредед, то возможно имеет смысл отключить гипертрединг, чтобы максимально загрузить реордер буффер и пайплайны.

Это тоже не ускорение кода, а скорее убирание помех.

для начала было бы неплохо знать что откуда копируется?

for(size_t it = 0; it < sz; it++)

Я же сказал, что компилятор не будет оптимизировать, пока неизвестен размер. Поэтому нужно применять константы, если размеры блоков известны, если неизвестны, то как-то так, определи сам оптимальный размер блока.

blocks = sz / 1024 / 1024;
rem = sz % (1024 * 1024);

for(size_t jt = 0; jt < blocks; it++)
{
for(size_t it = 0; it < 1024 * 1024; it++)
{
		dst8[it] = src8[it];
}
}

for(size_t it = 0; it < rem; it++)
{
		dst8[it] = src8[it];
}

Добавь -ftree-vectorizer-verbose=2 и перекомпили только этот модуль, оно напишет, если были ошибки оптимизации и оно не смогло оптимизировать.

сейчас это -fopt-info-vec-missed

Я посмотрю ещё поздно вечером сегодня, но пока лучшие результаты я получил с этими опциями:

g++ mcpy.cpp -O3 -ftree-vectorize -msse4 -fno-builtin -std=c++11 -ftree-vectorize -funroll-all-loops -fexpensive-optimizations -ftree-loop-distribution -fopt-info-vec-missed

Почему-то оно по-умолчанию не разворачивало циклы. Но ошибок оптимизации компилятор даёт массу, я посмотрю сегодня вечером причины.

Как подбирать пределы для циклов?

Это очень интересный вопрос и зависит от версии компилятора. В некоторых случаях оно разрешает использовать расширенный набор XMM регистров, а в некоторых нет. В общем я добился, чтобы код компилировался максимально правильно, но не идеально. auto-vectorization очень хрупкая штука.

Оно генерирует код подобно этому (внутренний цикл из примера ниже):

.L7:
        movdqa  (%rax), %xmm7
        subq    $-128, %rax
        subq    $-128, %rdx
        movdqa  -112(%rax), %xmm6
        movdqa  -96(%rax), %xmm5
        movdqa  -80(%rax), %xmm4
        movdqa  -64(%rax), %xmm3
        movdqa  -48(%rax), %xmm2
        movdqa  -32(%rax), %xmm1
        movdqa  -16(%rax), %xmm0
        movaps  %xmm7, -128(%rdx)
        movaps  %xmm6, -112(%rdx)
        movaps  %xmm5, -96(%rdx)
        movaps  %xmm4, -80(%rdx)
        movaps  %xmm3, -64(%rdx)
        movaps  %xmm2, -48(%rdx)
        movaps  %xmm1, -32(%rdx)
        movaps  %xmm0, -16(%rdx)
        cmpq    %rax, %rcx
        jne     .L7

Плюс по сравнению с ассемблерной вставкой — генерируется меньше кода обвязки (пролога, эпилога).

void memcpy_MG(void* __restrict__ dst, void* __restrict__ src)
{
      uint64_t* __restrict__ dst64 = (uint64_t*) __builtin_assume_aligned(dst, 16);
      uint64_t* __restrict__ src64 = (uint64_t*) __builtin_assume_aligned(src, 16);

      for(size_t jt = 0; jt < 1920 * 1080 * 3 / 128; jt++)
      {
              for(size_t it = 0; it < 16; it++)
              {
                      dst64[jt * 16 + it] = src64[jt * 16 + it];
              }
      }
}

g++ mcpy.cpp -O3 -msse4 -march=core2 -fno-builtin -std=c++11 -ftree-vectorize -fopt-info-vec -fno-strict-aliasing

Компилятор отчитался, что полностью справился без дополнительных условий, ограничений и ошибок:

mcpy.cpp:28:24: note: loop vectorized
mcpy.cpp:28:24: note: loop vectorized
mcpy.cpp:28:24: note: loop vectorized
mcpy.cpp:28:24: note: loop vectorized

В качестве альтернативного варианта можно использовать без автовекторизации почти с той же скоростью.

#include <xmmintrin.h>

void memcpy_MG(void* dst, void* src)
{
        __m128* __restrict__ dst128 = (__m128*) __builtin_assume_aligned(dst, 16);
        __m128* __restrict__ src128 = (__m128*) __builtin_assume_aligned(src, 16);

        for(size_t jt = 0; jt < 1920 * 1080 * 3 / 128; jt++)
        {
                for(size_t it = 0; it < 8; it++)
                {
                        dst128[jt * 8 + it] = src128[jt * 8 + it];
                }
        }
}

Только в случае, если данные не выровнены, наверное, в обоих вариантах есть смысл сначала докопировать недостающие до выравнивания байты вручную по одному?

На скорость это не повлияет, на фоне копирования 3 мегабайт данных, зато гарантирует остутствие трудноповторимых багов в процессе использования.

Там выравнивание заложено в самом дизайне (я про пример вверху), память выделается уже выравненная и указатели приводятся к выравненному состоянию, чтобы gcc не смущать.

да, но c if-ом и маской код будет будет отрабатывать на пару тактов медленнее, чем в контексте копирования мегабайтов в памяти можно и нужно пренебречь. А вот тем, что в функцию можно передать невыровненный указатель и тем, что работать в этом случае код будет непредсказуемо — я бы пренебрег только если это fixed-price write-only код

невыравненные указатели можно отловить на гораздо ранней стадии чем memcpy, ну и назвать функцию можно так, чтобы сомнений было много, прежде, чем её вызвать. Когда идёт оптимизация такого рода, то в ход идут все ухищрения.

да, но c if-ом и маской код будет будет отрабатывать на пару тактов медленнее, чем в контексте копирования мегабайтов в памяти можно и нужно пренебречь.

Хотелось бы также посмотреть на код, который универсально отрабатывает кейсы невыравненных указателей:

src+5
dst+3

и

src+2
dst+6

простейший вариант выглядит так:
if(0 != ((src | dst) & 0x3F)) memcpy(/*std::memcpy*/); else { ... }
еще можно сначала скопировать минимум байт по одному до границы выравнивания, затем скопировать ту часть src, которая идеально выровнена, и потом отдельно по одному докопировать байты после дальней границы выравнивания. Универсально и не сложно.

Можно добавить assert().

Можно переименовать.

Под невыровненный dst ничего оптимизировать не надо. Но вот чего точно не надо делать — это код, который будет работать тихо и не правильно.

и потом отдельно по одному докопировать байты после дальней границы выравнивания.

Это не оптимизация уже. Смысла в ней нет.

Под невыровненный dst ничего оптимизировать не надо.

Почему? А если невыравненный dst попадёт на границу кеш-лайна, то потери будут такие, что неоптимизированный побайтовый копи будет быстрее.

Но вот чего точно не надо делать — это код, который будет работать тихо и не правильно.

Данный SSE код точно не будет работать тихо и неправильно, оно упадёт.

Почему? А если невыравненный dst попадёт на границу кеш-лайна, то потери будут такие, что неоптимизированный побайтовый копи будет быстрее.

Потому, что это не возможно в общем случае. А усложять код с надеждой на везение и «выравниваемую» ошибку по-моему нет смысла.

Либо пользователь вызывает метод правильно и все у него работает быстро, либо — неправильно, и в этом случае есть смысл в отладке вывалиться на assert(), а в релизе — работать, но медленно.

Данный SSE код точно не будет работать тихо и неправильно, оно упадёт.

Если упадет, то норм. Но почему упадет? Насколько я помню, SIMD на x86 вполне себе переваривают невыровненные данные, но делают это намного медленнее.

Насколько я помню, SIMD на x86 вполне себе переваривают невыровненные данные, но делают это намного медленнее.

Есть aligned версии инструкций, они быстрые и падают, есть unaligned — они медленные и не падают. Это тоже одна из причин уверить gcc, что все указатели выравненные, чтобы он не беспокоился и не генерировал две-четыре версии одного и того же кода под все случаи жизни.

Есть aligned версии инструкций, они быстрые и падают

о, спасибо.

Та ну. Если пишешь алгоритм обработки картинок, и сам их выделяешь.

и который никто не будет поддерживать. Или когда пофиг, кто там и как будет его поддерживать. Тогда не вопрос.

Для поддержки такого кода нужны нормальные пререквизиты в виде скиллов, если их нет, то ничего не поможет. Код для чайников находится в другом месте %) У нас есть есть софтварный фолбек для некоторых аппаратных операций и так часто случается что софтварный фолбек работает быстрее аппаратуры во многих особых случаях. Только минус его в том, что загрузка ядра на 100%, в то время как даже медленная аппаратная реализация не загружает процессор вообще.

Эта библиотека попутно является так называемой «ямой позора». Каждый, кто почувствовал, что знает коде-фу пытается её оптимизировать и на кодеревью постоянно возникают жаркие споры, когда новичок пытается доказать что его код правильнее и быстрее. Когда мне надоело я положил в проект бенчмарк, и по правилам ни одно коуд-ревью в той библиотеке теперь не может быть без указания значений бенчмарка до и после изменений.

С тем, что скилы нужны — я не спорю. И хорошее самувствие, и ответственность, и внимательность :) Но на практике то, что «надо» и то, что «есть» иногда отличается и я считаю, что хороший код в случае таких несовпадений должен как минимум ругаться плохили словами в дебаге.

Вопрос был в том, стоит ли игра свеч, когда усложнение кода ценой выигрыша в пару тактов повышает риск ошибок по невнимательности.

Вопрос был в том, стоит ли игра свеч, когда усложнение кода ценой выигрыша в пару тактов повышает риск ошибок по невнимательности.

Когда аутомотив кастомер выбирает платформу за 5 лет до релиза и через 3 года разработки он понимает, что ему не хватает 10% производительности для полного счастья. А на кону стоят сотни миллионов, то в позу становятся все, независимо от того стоит игра свеч или нет %)

Я такого не видел, могу только спросить, это примерно так?

— we need performance improvement by 10%
— [click-click-click (160h elapsed)] unfortunately, it’s impossible, but we managed to improve it by 1.0%

По аналогии с:
— Люся, я люблю тебя с 8го класса, выходи за меня!
— а я тебя нет, но знаешь, я срадостью буду дружить с тобой и напоминать о себе следующие 50 лет!

ну, т.е. не „impossible”, конечно, мы ж специалисты. А что-то вроде „we have to decrease accuracy by 30% in order to speedup by 10%”. Но суть та же.

— [click-click-click (160h elapsed)] unfortunately, it’s impossible, but we managed to improve it by 1.0%

Если кастомер из Японии тебе вежливо предлагают самовыпилиться — die with honor %) На самом деле всегда можно выжать даже гораздо больше только подойдёт ли запрашиваемая цена.

выбирает платформу за 5 лет до релиза

однако, планувати на 5 років за сучасних темпів розвитку технологій — таки дещо самонадіяно.

однако, планувати на 5 років за сучасних темпів розвитку технологій — таки дещо самонадіяно.

Например, я начал работать с Intel ApolloLake за два года до официального выхода платформы как раз по этой причине. Ну и long term support от вендоров. Это же не десктоп.

Когда мне надоело я положил в проект бенчмарк, и по правилам ни одно коуд-ревью в той библиотеке теперь не может быть без указания значений бенчмарка до и после изменений.

Моё уважение!

Алсо, ты бы записал бы в ту память перед копированием, а то ляликс не факт что даже страницы памяти выделил

Алсо, перед тестами дропай/подтягивай кеши

Да один раз memset на всю память после выделения и потом уже с ней работать.

бачу, що все ж напрошуєть компілятор Інтел, і можливо, асемблерні вставки для роботи із графікою SSE AVX чи що там є зараз
вот нагуглив
habrahabr.ru/post/99367
там в кінці
Ускорение до 80% с помощью библиотеки MKL чисто на вычислительных задачах — это очень неплохо!

ще ось книжка, правда старувата
books.google.com.ua/...​intel sse графика&f=false

стаття
habrahabr.ru/...​ompany/intel/blog/245755

рсдн
rsdn.org/forum/cpp/5178586.flat

Майк, а не посоветуешь, как вообще правильно с видеофреймами работать, чтобы вычисления быстрее были.

Всё зависит от того, откуда ты эти фреймы берёшь. Если они изначально в GPU домене, то там надо и оставаться для максимальной производительности.

Вот простой пример imabsdiff,

А что там внутри? Это intensity = 1/3 (r+g+b)? Это lightness физической модели? Это дистанция sqrt(r^2 + g^2 + b^2) ? Это перцептуальная модель аналогового телевидения 0.299R+0.587G+0.114B? Цифрового телевидения 0.21R+0.72G+0.07B? От этого зависит то, как ты будешь обрабатывать данные. Если источник — это камера или джпег, на кой чёрт его надо конвертить в RGB вообще? Пусть остаётся в YUV.

Ну и imabsdif = abs(img_1-Img_2). Ну а мне нужно еще среднее этогo imabsdiff. Но направление на что обращать внимание ты уже подсказал.

Каждую компоненту отдельно? R-R, G-G, B-B ? Если да, то напиши простой цикл по байтам для массива, который это делает. Оно прекрасно ляжет в MMX при включённых опциях оптимизации.

-Wno-unused-result-O3

Я надеюсь, это раздельно?

Алсо нет -march=...

Сейчас подтянутся фронтэндщики и все посоветуют и порешают.

сам по собі gcc генерить гавно-код з достатнім оверхедом,
пробуй пропроетарні компілятори або LVCC

до чого інтрістікі, якщо я тобі раджу взяти замість широкопрофільного комбайна узкозаточений інструмент (замість gcc пропроетарний компілятор, або спробувати хочаб із LVCC)

також використовуй декрементний цикл, а не інкрементний, є різниця в швидкості

в мене варіантів більше нема

використовуй std move або swap

я не знаю контекст задачі, так що більше нічого нема порадити
загалом, нащо копіювати, якщо може достатньо просто передавати вказівники на буфери,
ще міг би порадити DMA, но в даному випадку, думаю, це не релевантно, так як тре лізти в лов-велел кодінг

Ты бы лучше сказал, зачем тебе гонять гигабайты по памяти? Может можно алиасить или какие-то другие трюки применить?

ще можна вирівнювати дані до розміру sizeod (*void)

так для чого ганяти гігабайти?

Это всё непереносимые извращения. К тому же, с малым выигрышем (если вообще). В общем, забей.

Есть конечно.
Дружеский совет — склонируй себе сырцы libav, там в основном подобные оптимизации уже реализованы на портабельном уровне.
Ну и общее правило по работе с видео — никогда не копируй кадр или план, если есть хоть какая-то возможность этого не делать. ;)

А тебе вообще нужен этот memcpy в том виде, в котором он есть? В смысле, с контролем выхода за границы массивов, с пересчётом указателей, с выравниваниями и тайпкастами? Как насчёт ассемблерного кода — просто рассказать процу скопировать данные из пункта А в пункт Б? Я бы навскидку смотрел команды lods, stos, но может быть есть что-то получше в той архитектуре, под которую ты пишешь.

Дальше, методика замеров кривая. Не хочешь просто дать программе прямой код в бесконечном цикле, с охрененно большим массивом, скажем 128Мб, и сказать колбасить пока колбасится? А потом просто прочитать количество циклов за единицу времени (скажем, минуту)? Ну или просто лимитнуть количество 10000 циклами? Почему так: оценка времени — процедура не самая быстрая. Вызов функции — процедура небыстрая (если понимаешь, что такое стэк, что такое очистка регистров, и т.п.).

В общем, если ты готов тупо выделить буфера фиксированной длины с абсолютными адресами, и оперировать большими кусками данных как одним целым — спокойно бери ассемблер и смотри как до тебя это делали.

Если бы мне стояла подобная задача — я бы в первую очередь оптимизировал данные, а не функцию: чтобы за один раз шло копирование как можно большего куска данных. Чтобы этот самый memcpy сделать инлайн-функцией, если речь всё же о коротких кусках. И чтобы вообще не было передаваемых аргументов, циклов, а был как бы один тип данных, занимающий N байт, находящийся по void-указателю.

Экий ты забавный спамер. :)

Где ты увидел «контроль выхода за границы массивов, с пересчётом указателей, с выравниваниями и тайпкастами»? Тупое копирование в цикле, без каких-либо чеков — это не контроль, а его полное отсутствие.

Мемкопи приблизительно так и реализован — побайтным копированием в цикле. На ассемблере.
Некой магической команды «скопируй мне область памяти в другое место», без копирования в цикле — нет.

Я не спорю, часть нюансов возможно оптимизирует компилятор при константных параметрах. Но какое отношение ко всему этому имеешь ты, тролёныш рашистский?

Если бы я предметно и недавно занимался фильтрацией 2D, то рассказал бы.
Но в плане перекачки данных — тебя явно натолкнули на мысль, читай что получилось в ассемблерном коде.

Я согласен, что иметь ассемблерную вставку — не всегда хорошо, особенно с точки зрения читаемости. Но по крайней мере подобрать код, делающий в аккурат то что тебе надо, и не делающий лишних телодвижений — то что доктор прописал!

Вот к примеру, вызов функции — на кой чёрт он тебе, ЕСЛИ ты точно знаешь что это стековая операция? Что тебе мешает сделать эту функцию инлайновой, переменные передать по ссылкам? Если вообще передавать, не лучше ли макроподстановка препроцессором на компиляции?

Точно так же, как для тебя неочевидна редукция данных для целей аналитики, так почему-то неочевидна механика кешей проца, особенно такого зверя как GPU. Уже не раз кажется повторял — избавляйся лишних ветвлений, старайся писать линейный код — и получишь прекрасную оптимизацию.

Да, у компилятора есть «синтаксический сахар», позволяющий тебе написать цикл, который потом будет развёрнут в линейный код. Но ты должен убедиться, что компилятор смог развернуть код. Что ты передал константные параметры, а не подсунул потокобезопасную переменную. Вплоть до того, что ты руками можешь развернуть циклы ради эксперимента — это ведь несложно.

Лично мне в принципе непонятен твой затык в копировании массивов данных. Но это может быть потому, что я не знаком с твоей задачей, только и всего. Если на CPU, то очень выгодно повторное использование данных. Если на CPU->GPU — то смотри в сторону DMA, то есть не будет ли лучше с точки зрения проца передать функцию прокачки данных контроллеру шины и скармливать постранично. Опять же, всё сильно от линейности данных пляшет.

Простой пример — считывание элемента массива по индексу. Ты в курсе, что программе нужно ещё две проверки на то, не вышел ли твой индекс за границы массива? Да, это тебе очевидно. А программа понятия не имеет, что ты пришлёшь в рантайме. А вот заменил бы операцию с массивом на операцию с указателем — и вуаля, прирост! А когда ещё и операция с умножением и сложением указателя не требуется, компилятор уже посчитал относительную константу — ещё парочку операций сократил. И это на каждом элементе массива!

Ещё неочевидный затык, «детская» ошибка — это полагаться на операционку в вопросах адресации памяти. В операционке память постраничная, доступ виртуализирован, вплоть до того что страница может оказаться в свопе. Как догадываешься, это тоже подтормозка. И чтобы её обойти, надо попросить у операционки кусок неперемещаемой памяти, и работать уже с ней.

В случае работы на GPU она у тебя фиксирована, беспокоиться не стоит.

тогда асемблер, ДМА, подвійні буфери, шелов копі, статичні масиви....

мабудь що ні, даташит та апнотес на процесор та загальну літературу про те як відрисовувати буфер(и) відеокартинки в пам"яті а потім свопати із активним буфером який відрисовується,
я б сказав що це дуже конкретно зав"язано на апаратну платформу

Якщо це звичаний ПК то тре ще лізти в роботу PCI (-e) та інших бортових шин, але я давно і мало займався десктопними архітектурами, так кастом солюшен на ARM-DSP 10 років тому для відрисовки графіки на LCD дисплей

Типичный пример — распознавание образов. Если его делать не в GPU, то фрейм нужно забрать из устройства, т.к. как правило там некешируемая память и читать напрямую из неё очень медленно. Гораздо быстрее скопировать в системную память и потом работать с образом.

Підписатись на коментарі