Комментарии 29
различие между методом
atиoperator[]уstd::vectorизменения порядка циклов с
ijkнаikj
Все бы хорошо, да вот только когда речь идет о перфомансе - примеры есть? Где микросекунды? Верить на слово?
В статье опишу "набор новичка"
Есть упоминание об инструменте - нет примера использования. С одной стороны какие-то уж очень общие знания, с другой - переключение на "углубленные" нюансы С++.
Верить на слово?
Спецификация at, [] есть в стандарте, в статье есть ссылка на недавнее выступление с cppcon про матричное умножение, ну или вот есть статья на хабре.
Есть упоминание об инструменте - нет примера использования
Ну, чем богаты.
У меня тоже сомнения. ARM архитектура, язык C (такие были всегда требования).
Проверял, *dst=*src работает быстрее, чем dst[i]=src[j]. Массивы с указателем data[i] внутри (ассемблер) не смотрел. Но если они работают с байтами, то у них должно быть более сложное устройство кода. А вот указатели - смотрел. С байтами они не работают, в частности, была ситуация, память могла работать только с 32-битными словами. Если на шину адреса выставить число, не кратное 4-м - то получал исключение. В таком случае выход такой - делается на Ассемблере, проверяется выравнивание (на 1, 2. 3. 0) и производится переход на метку для этого случая. Работаем с 32-разрядными адресами, считывам данные, потом выделяем из них нужные байты и т.д. Чтобы было быстрее, применяем команду PLD (preload). Так и системная memcpy работает (не со всеми SoC). Но оно того стоит - скорость выше.
А теперь - немного про caches. На днях запустил микротрансформер на "голом железе" на Raspberry Pi Zero 2W. (Cortex-A53, 64 бита, 4 ядра). Сначала без кешей, мне надо было код изменять раньше. Работало, медленно. Включил кеши и MMU. Вместо ожидаемых 10...20 крат прироста скорости, получились все 40. Вот тут я начал сомневаться, а что если процессор берет данные не из ячеек памяти, где они актуальные, а из кеша и помещает также, где гарантия, что они - валидны. Тут есть вариант либо чтения несколько раз, пока не считается нужное, либо надо ставить барьер в памяти. И то, и другое -лишние циклы. А если несколько ядер, у каждого свои кеши - как они могул знать, что у кого в "мозгаз" пока не попадут в RAM.
На "голом железе" легко можно включить/отключить кеши и сравнить. А в случае OC и Python? (Когда-то кеши можно было вклчать в BIOS, там же настраивать write-back, write-through). Были сообщения о дискредитации научных работ на Питоне, тогда списали на "разное представление чисел с плавающей запятой в разных процессорах". А что, если кеши виноваты - кто проверял.
P.S. На видео (RuTube) видно, как ядро 0 работает в сравнении с остальными:
https://rutube.ru/video/55c288c96846fd3402f6bf892df65f90/?r=a/
Отличная тема для статьи кстати.
*dst=*src работает быстрее, чем dst[i]=src[j]. Массивы с указателем data[i] внутри (ассемблер) не смотрел
не совсем понял о чем речь
Включил кеши и MMU. Вместо ожидаемых 10...20 крат прироста скорости, получились все 40
Почему ожидали 10..20?
Вот тут я начал сомневаться, а что если процессор берет данные не из ячеек памяти, где они актуальные, а из кеша и помещает также, где гарантия, что они - валидны. Тут есть вариант либо чтения несколько раз, пока не считается нужное, либо надо ставить барьер в памяти. И то, и другое -лишние циклы. А если несколько ядер, у каждого свои кеши - как они могул знать, что у кого в "мозгаз" пока не попадут в RAM.
Как-будто вы описываете общую проблему валидности памяти кэш систем с разделенными L1/L2. Отсюда кстати и false sharing вырастает
Корректнее было сравнить dst[i]=src[j] с *(dst + i) = *(src + j).
Согласен. это было "образно". Думал написать *dst++=*src++ или dst++=some_data, но при вводе "*" тут текст становится курсивом, и это сбило с толку.. Речь, собственно о том, что при работе с указателями получалось быстрее, чем при работе с массивом с индексом.
Это в общем от компилятора зависит, если взять один и тот же код и скомпилять, скажем, Студией, интеловским OneAPI и gcc, то может быть быстрее и так и сяк, вот как раз интеловскому индексы могут "нравиться", ему с ними векторизовать легче. Это даже от версии компилятора может зависеть, прогресс на месте не стоит. Я обычно нагруженные циклы профилировщиком смотрю, и в принципе при некоторой "насмотренности" по ассемблерному коду уже более-менее видно, надо ли менять одну форму на другую или нет.
Вроде как раз для таких случаев godbolt.org полезен чтобы посмотреть что будет с -O2/-O3
Разве в C это не полный синоним по определению оператора []?
Я думал, что имелось в виду
for (int i = 0; i < N; ++i) {
*(dst + i) = *(src + i);
}
и
while (src < end) {
*dst++ = *src++;
}
#include <stdio.h>
int i, number=777;
int src_data [20000000] = {0,1,2,3,4,5,6,7,8,9, };
int dst_data [20000000] = { };
int *src_ptr, *dst_ptr;
int main(void)
{
src_ptr = (int *)src_data;
dst_ptr = (int *)dst_data;
for ( i=0; i<20000000; i++) { dst_data [i] = src_data [i]; }
//for ( i=0; i<20000000; i++) { *dst_ptr++ = *src_ptr++; }
//dst_ptr = (int *)dst_data;
//for ( i=0; i<20; i++) { number = dst_data [i]; printf("num: %d\n", number ); }
return 0;
}Примерно одинаковая скорость, если закомментировать строку 15 и раскомментировать строку 15. Точно - компилятор по своему делает, возможно, один и тот же ассемблерный код вставляет. Это на Intel. На ARM потом попробую. Раньше было так, что через указатели быстрее, но надо дихассемблировать.
Чтож вы мучаетесь то
https://godbolt.org/z/177jcYKc4
P. S. Для личного опыта интересно потыкаться, но для прода я бы не выпендривался и использовал std::copy или std::ranges::copy
Не мучаюсь. просто когда-то давно выяснял, что лучше. А C++ стараюсь не использовать (и требования часто такие), а в C - есть memcpy, я ее и внутри смотрел, и свою сделал, чтобы четко работала. (Иногда попадаются процессоры с несколько нестандатной памятью, например, которые только кратные адреса 4-м терпят на шине). А еще бывает, что на "голом железе" функции некоторые вообще не работают из стандартных библиотек или большие, приходится свои делать.
MS VC еще буквально в прошлом веке такое оптимизировал до одинакового кода, только цикл делал с N вниз, сравнивая счетчик с 0.
Сейчас не так важно что за SIMD 128-512 поддерживается, так как в железе может быть 4х 128битных пайплайна, то есть 4 инструкции выполняются параллельно. Такое сделано например на Intel E-ядрах и Cortex X4. А на AMD Zen4 был SIMD256 dual issue для эмуляции SIMD512 пока они не сделали полноценные 512 бит на Zen5.
Очень сильно не согласен. Во-первых, SSE, AVX2 и AVX512 различаются не только длиной регистра, но и доступными инструкциями (вики и список интринсиков). Во-вторых, в зависимости от задачи ботлнек может быть по памяти, а может и по вычислениям, в последнем случае имеет значение сколько и каких SIMD юнитов имеет процессор. У меня есть пример где ботлнек по арифметике и как раз использование GFNI из AVX-512 ускоряет в два раза универсальный алгоритм на AVX2
https://github.com/Malkovsky/galois?tab=readme-ov-file#vector-operations
(понятно, что оба варианта еще можно соптимизировать и разница возможно будет меньше, но суть от этого не меняется)
Под AVX тоже есть расширения, например AVX-VNNI для Intel, который повторяет AVX512-VNNI.
Я все же про то, что решает распараллеливание инструкций, а не длина SIMD.
Ещё, кстати, AVX 512 может заметно дропать частоту ядер (ему просто больше транзисторов в моменте переключать надо), и по итогу выигрыш заметно меньше ожидаемого. Я как-то кучу времени угрохал, перекладывая AVX2 на 512 (команды там и правда разные и иногда просто тупой сменой ширины регистров не обойтись) и был сильно разочарован приростом, в два раза там и близко не было.
Ещё, кстати, AVX 512 может заметно дропать частоту ядер (ему просто больше транзисторов в моменте переключать надо)
Хмм, я мне говорили, что он это делает, потому что первые релизы плавили процессор.
Может и так, я где-то читал, как оно там устроено, он температуру не меряет, а просто смотрит на команды, сколько их через сколько ядер летит и соответственно частоту снижает, там просто таблицы, то есть это не термальный троттллинг, который в теории может и на успеть. Хасвелл вот и на командах AVX2 до максима не бустился.
Паттерны доступа к памяти сейчас важнее, чем количество тактов на арифметическую инструкцию. CPU считает быстро, но ждет данные из RAM целую вечность. AoS vs SoA - вот где зарыты реальные иксы производительности
А архитектура CPU — совсем-совсем без разницы (т.е. примеры и соображения в статье не зависят от того, выполняются они на amd64/ARM/RISC-V)?
Разница в первую очередь в наборах инструкций, AMD64 и x86-64 в этом плане примерно одно и тоже, но у AMD поддержка новых расширений частенько с запозданием. ARM и RISC-V -- это семейство RISC, у ARM свои SIMD (ссылка есть в статье), с RISC-V совсем не работал, поэтому гадать не буду -- лучше поищите сами.
Референсные значения по таймингам доступа к кэшам/регистрам схожи, но если вы оптимизируете под конкретное устройство, то нужно смотреть референс этого конкретного устройства.
result = condition if_expression + !condition else_expression;
кстати в java и варинат "condition ? if_expression : else_expression" работает, компилируется в conditional move, наверно и других языках похожая ситуация. это я к тому, что выражение "? : " совсем не обязательно компилируется в чистый if, выглядит не так жутко как предложенный вариант и работает с такой же скоростью

Введение в высокопроизводительные вычисления на С++ для CPU