Pull to refresh
16K+
24
Дмитрий@dsbasko

User

54,1
Rating
22
Subscribers
Send message

Проверил дизасмом на обеих машинах, выравнивание оказалось ни при чем: загрузки его попросту не требуют. Go гарантирует слайсу float32 только 4 байта, поэтому компилятор выровненных инструкций и не генерирует. На arm64 в горячем цикле стоит FMOVQ без требований к адресу, на amd64 загрузка вообще слита со сложением в VADDPS 0(DX), Y1, Y1, а VEX-кодировке все равно, выровнен ли адрес. Про Itanium вы правы, тот бросал фолт. Современные x86 и arm64 прощают, небольшой штраф остается лишь на пересечении кэш-линии.

Причем в этой задаче загрузки стоят ноль тактов, что видно из замера: Go-цикл со всей обвязкой идет 2.62-2.65 такта на вектор, а голая регистровая цепочка fadd без обращений к памяти идет 2.66. За «работает, но не так скоро» отвечает цепочка зависимостей. На одном и том же массиве 4 МБ наивный цикл занимает 693 мкс, двенадцать скалярных аккумуляторов 159, вектор 102, хотя загрузки и выравнивание везде одинаковые, менялся только порядок сложений. Подробный разбор в секции про декомпозицию и в UPD.

Пик у меня замерен на этом самом M3, а не взят из таблиц. Шестнадцать независимых цепочек fmla дают ровно 4 инструкции за такт, те самые 16 fmad по fp32. Только выходит 129 ГФлопс на ядро, не 256: 16 x 2 флопа x 4.03 ГГц.

Проблема достигнуть пика в простом цикле есть, причем у любого языка. Пику нужны 4 независимые инструкции каждый такт, а сумма тащит одну цепочку, где следующее сложение ждет предыдущее. Потолок такой цепочки равен 1/латентность, то есть 0.38 инструкции за такт, девять процентов пика, хоть на ручном ассемблере. Чтобы прокормить все блоки, аккумуляторов нужно блоки x латентность, около двенадцати, и плато в замере наступает как раз на двенадцати цепочках (microbench/ports_neon.c в репозитории). Сам компилятор так разложить цикл не имеет права: переассоциация float меняет результат, Go под честным IEEE 754 на нее не идет.

Что цикл упирается не в память, видно на одном массиве 4 МБ. Наивный цикл 693 мкс, шестнадцать скалярных аккумуляторов 155, вектор с четырьмя 102. Чтения во всех трех версиях одни и те же, ускорение в 6.8 раза дала одна лишь перестановка сложений.

С вашими 4 simd-аккумуляторами в Go так и вышло: версия с четырьмя лучшая на миллионе, 102 мкс, замер уже уехал в статью. Но работает четверка не потому, что блоков четыре. Чистым цепочкам для насыщения нужно двенадцать, просто Go-вектор упирается в обвязку раньше, чем в порты, и восемь аккумуляторов на миллионе уже чуть медленнее четырех. По пикам i9 и M3 равны, об этом в статье прямо написано. В этой задаче их различает латентность звена цепочки, 2.03 против 2.66, оба числа замерены.

Про AMD согласен, поэтому в конце статьи и стоит просьба к владельцам Zen 4/5 прогнать бенчи. Ну и касаемо AMD. Мерил на том что есть :-)

Как и обещал. Статью доработал.

Спасибо за теплые слова! Я очень рад, что смог нанести вам пользу :-)

Спасибо за теплые слова!

Сейчас больша часть времени уходит на доработку систем SSO и AMC (access control management), и кандидата я вижу именно в них. Деталей рассказать не могу, но область назову.
Горячий пусть авторизации - это проверка прав и согласий на каждый запрос. По сути пересечения множеств коротких слов, пачками до тысячи элементов.
Горячий узкий цикл с данными, которые влезают в кэш. В целом там все готово, осталось замерить на реальном железе, у нас на серверах кстати поддерживается AVX-512.

Если будут интересные цифры, доработаю статью.

Спасибо за разбор, это лучший фидбек который статья могла получить, развернуто и с цифрами. Часть пунктов настолько справедлива что уйдет в правки статьи. По части принесу встречные замеры.

«Берет частотой» признаю, моя недоработка. Частота объясняет примерно 1.5x из двукратной разницы на скаляре, 6.0 ГГц против ~4.05. Остальное добирает латентность цепочки FADD, 2 такта у Raptor Cove против 3 у M3 (uops.info по ADDSS и таблицы dougallj по Firestorm). Пересчитал свои же числа, на i9 выходит ~2 такта на элемент (в чистом замере при 5.7 ГГц ровно 2.03), на Mac ~2.8. То есть i9 и правда делает больше работы за такт а не только чаще тикает. Этого пересчета в статье нет, и зря.

Про стену памяти соглашусь с акцентом. Mac не «не имеет стены», он до нее не доезжает. Я писал что код упирается в цепочку сложений а не в память, но ваша формулировка честнее моего заголовка. Ядро M-серии тянет из DRAM порядка сотни ГБ/с (Anandtech намерял ~102 на M1 Max), моя сумма ест 23-24. Проверил и пункт про DRAM против кэша, у i9 из памяти 0.167-0.169 нс на элемент, у Mac из кэша 0.173. Действительно не медленее. Вот это я упустил полностью, даже не думал в эту сторону.

Теперь с чем не соглашусь, это «6 портов для скалярных FP» и потолок 16/6 ≈ 2.66x.

По uops.info скалярный ADDSS и векторный VADDPS ymm у Golden/Raptor Cove живут на одних и тех же двух портах p1 и p5. У обоих латентность 2 и темп 2 инструкции за такт. Шестерка похоже пришла из соседней ячейки памяти, пять целочисленных ALU или общие 12 портов ядра.

Проверил и железом на том же P-ядре что в статье. Чистые регистровые цепочки на C без обвязки Go, частота во время прогона 5.70 ГГц по сэмплеру.

sc_add_1      2.00 такта/инстр      латентность ADDSS
sc_add_4..12  1.98-2.01 инстр/такт  плато, 2 порта
v_add_4       2.02 инстр/такт = 16.1 fp32/такт

Умей скаляр 6 сложений за такт, двенадцать независимых цепочек показали бы 0.029 нс на сложение. Показывают 0.087. Так что портовый потолок вектор/скаляр получается 16/2, те самые 8x. Только это потолок пропускной способности когда зависимости развязаны с обеих сторон, а не «сколько даст вектор поверх наивного цикла». Кстати практикой 2.66x тоже не подтверждается, если развязать цепочки в обеих версиях, мои Go-замеры дают 1.4-1.8x, еще меньше.

Похожая история с «процессор развязывает зависимости и без вас, просто менее эффективно». С этой цепочкой так не получается. Замер дает 2.03 такта на элемент, ровно латентность сложения, аппаратной помощи ноль тактов. OoO прячет под цепочку загрузки и счетчик цикла, но переставлять сложения float ему запрещено, поменяется результат округления. Поэтому аккумуляторы не подсказка процессору, а разрешение на переассоциацию которое может выдать только программист.

Откуда тогда 5x вместо 8x. Дизассемблировал горячий цикл sumSIMD@simd256, на одну VADDPS там 17 инструкций, bounds check и пересчет указателя слайса на каждой итерации. Итерация выходит за 3.2 такта вместо латентностных двух. 8 полос × 2.03/3.21 = 5.06x, сходится с бенчмарком до второго знака. На Mac та же обвязка прячется под латентность 3, отсюда его ровные 4x. Вобщем виноват кодоген simd-пакета, а не порты.

Про впечатление «Mac эффективный, а Intel не очень». Такой задумки не было, но перечитал заголовок и секцию про стену вашими глазами и вижу откуда оно берется.

Фидбек воистину полезный и с ним я что-то буду делать. Спокойно перемерю все еще раз и доработаю статью, добавлю пересчет на такт с латентностями и честный разбор куда деваются иксы. Заодно хочу добрать замер с 12 аккумуляторами на Mac, поидее M3 с латентностью 3 и четырьмя FP-конвейерами должен раскрыться именно там, на это я не расчитывал когда писал про восемь. Еще раз спасибо за пункт про DRAM против кэша и за пинок пересчитать все в тактах, об этом я не подумал.

Такие комментарии могут быть ценнее самой статьи.

На меня больше всего произвело впечатление https://habr.com/ru/articles/995906/#simd-archsimd . Статью я писал долго, по этому не стал править. Но мне удалось потестить работу на Intel процессоре 14 поколения. Прирост в производительности почти x3. Это очень сильно.

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

P.S. Ждем тернарный оператор :-)

Да, sqlc отличный инструмент, но это разные весовые категории :-)

sqlc парсит SQL, генерирует Go-структуры и методы. Даёт типобезопасность, но добавляет шаг сборки, зависимость от внешнего тула и генерируемый код в репозиторий.

Мой же подход сейчас - это просто парсер на условно 50 строк кода. Ноль зависимостей, ноль генерации, полный контроль над кодом репозитория. Ты сам решаешь как сканировать результаты, какие хелперы использовать, как обрабатывать ошибки.

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

Хороший разработчик не привязывается к одному инструменту, а понимает компромиссы каждого.

Интересное решение. Даже не думал об этом :-)

Хранимые процедуры легитимный подход, но со своими компромиссами. Логика размазывается между приложением и БД. Для меня это усложняет отладку и понимание системы целиком.

Версионирование процедур отдельная боль. Нужно синхронизировать миграции БД с деплоем приложения.

Тестирование тоже усложняется. Для unit-тестов репозитория теперь нужна реальная БД с актуальными процедурами. Плюс vendor lock-in: PL/pgSQL !== T-SQL !== MySQL процедуры.

Странно что Не бойся использовать разные инструменты в одном проекте при чтении статьи проходит мимо глаз. Если у вас команда DBA сильная и процедуры стандарт, отлично. Embedded SQL для тех, кто хочет держать всю логику в Go и деплоить одним бинарником.

Константы в Go-файле - валидный подход. Но в чистом .sql файле работает полноценный SQL-тулинг: форматирование через pg_format, линтинг через sqlfluff, автодополнение колонок при подключении IDE к БД.

Парсер и риск ошибок. Регулярка тривиальная, 5 строк. Если разработчик забудет маркер, запрос не попадёт в map, и Get() упадёт с panic при первом же вызове. Ошибка не пройдёт незамеченной.

Ошибка компиляции. Тут я возможно действительно преувеличил. go:embed даст ошибку компиляции только если файл не найден. Однако никто кто мешает прикрутить нормальную валидацию SQL при инициализации пакета. Все это выходит на рамки простой статьи.

Как я писал в самом начали в конце статьи: это не единственный, да и не самый лучший способ организации работы с SQL файлами. Однако в большинстве случаев на моей практике он самый удобный.

Евгений спасибо, полезная статья! Было бы неплохо также детальнее разобрать мьютексы и библиотеку race (то как она работает под капатом). Думаю получился бы неплохой цикл статей по примитивам синхронизации.

Отличная статья, спасибо! Но кажется не работают якоря на содержании

В тексте ошибка.
Generics - это функции или типы, которые могут работать с любым типом данных. В Go generics были введены в версии 1.21 и они означают, что вы можете написать функцию, которая будет работать с любым типом данных, а не только с определенным.

Введены они были в 1.18

При написании пути можно использовать ~ для обозначения директории пользователя системы. По умочанию vscode складывает свои настройки в ~/.vscode. Я этого в описании не писал, сейчас напишу. Заодно лого поменяю, а то неудачное какое-то получилось )

Сделал бы раньше, но по проекту дедлайны горели.

Добавил возможность в настройках указать директорию.

Рад что понравилось ) Пиши если придумаешь что-нибудь интересное в процессе работы )

Я думаю такое можно осуществить

Кстати сделал. В версии 2.1.3 можно выделить текст в коде и на основе него создать компонент по шаблону. Более того, в названии компонента поддерживаются конструкции типа ../. Указав название шаблона как "../../modals/register user", расширение выйдет на 2 уровня вверх, создаст директорию modals и в нее положит компонент.

1

Information

Rating
129-th
Location
Ташкент, Ташкентская обл., Узбекистан
Date of birth
Registered
Activity

Specialization

Бэкенд разработчик, Архитектор программного обеспечения
Ведущий
TypeScript
Golang
PostgreSQL