Experimental chill
9.32K subscribers
12 photos
1 video
2 files
109 links
Algorithms, libraries, C++, Linux, Distributed Systems, maybe Rust

Donate: https://xn--r1a.website/experimentalchill/222

Author: @Danlark

Nothing in this blog is an opinion of my employer
Download Telegram
Мы тут выложили апдейт по тому, что будет с С++ в Google. В общем, на Rust у нас уже можно писать, Carbon все ещё разрабатывается, а с C++ мы будем включать bounds checks во всех приложениях.

Спойлер: включили bound checks во всяких векторах и строчках, немного CPU скушало, но все в пределах нормы. Были страхи, что встретим запрос смерти, который положит весь прод, пока не встретили, но появилась и у вас возможность уложить весь прод :)

https://security.googleblog.com/2024/10/safer-with-google-advancing-memory.html
🔥116👍33🤯117😁7🤣4❤‍🔥1🤔1
Experimental chill
Мы тут выложили апдейт по тому, что будет с С++ в Google. В общем, на Rust у нас уже можно писать, Carbon все ещё разрабатывается, а с C++ мы будем включать bounds checks во всех приложениях. Спойлер: включили bound checks во всяких векторах и строчках, немного…
https://security.googleblog.com/2024/11/retrofitting-spatial-safety-to-hundreds.html

Ну и в догонку очень быстрый апдейт. Включить bound checks отразилось всего в 0.3% регрессии по всему C++ гугла в среднем. Чтобы было так мало, мы ждали пока FDO (feedback driven optimizers) возьмут новые профили. В итоге кто-то меньше, кто-то больше, но я был доволен результатом. Количество сегфолтов в проде стало на 30% меньше. Сегфолты по разным причинам бывают и из-за тестовых бинарей в том числе (другой метрики у нас нет, поэтому репортим что есть).
👍89🔥49🤡7🤝53👏1🕊1
Отличный пост сегодня на hn, трюк знакомый, пугающий, и вроде даже в комментариях разобрались.
https://news.ycombinator.com/item?id=42579969
https://tavianator.com/2025/shlx.html

Автор поста обнаружил, что при таком патче в определённых условиях.

LOOP:
- MOV RCX, 1
+ MOV ECX, 1


Инструкция SHLX RAX, RAX, RCX (означающая RAX = RAX << RCX) ускоряется в три раза на Alder Lake.

Почему?

Решение этой загадки скорее всего заключается в том, что в Alder Lake увели много инструкций в стадию rename -- это когда процессор не ждёт регистра и его результата, а выдаёт новый готовый регистр (возможно с метаданными, например, что к нему надо что-то добавить). Например, лучше всего это видно в случаях циклов.

loop:
inc rax
cmp rax, rcx
jb .loop

Существуют две стратегии как переименовывать RAX, например

r1 = rax
r2 = r1 + 1
r3 = r2 + 1
...


Пока не закончатся физические регистры на самом процессоре. Можно слегка по-другому:

r1 = rax
r2 = r1 + 1
r3 = r1 + 2
r4 = r1 + 3
...


Тем самым вы делаете трейдофф между кодированием как выдать новый регистр и параллелизмом (r3 не надо ждать r2 и тд, но зато число 2 надо где-то закодировать). В реальности там скорее всего поддерживается до определённого лимита. Вы удивитесь, но вторую стратегию стали только применять с Alder Lake. Более удивительный вопрос, почему MOVE ECX работает, когда 64 битный RCX нет. Казалось бы :)

Там в целом можно упороться и в процессорах существуют регистры с константами, поэтому mov ecx, 1024будет быстрым, когда как mov ecx, 1023 медленным.

Почитать про этот трюк можно тут

https://www.computerenhance.com/p/the-case-of-the-missing-increment#footnote-anchor-6-149406677

Вот такие сюрпризы бывают.
🔥109🥱21👍17🙈12🤯9😁64😱4🤔2
S3 locality

Тут я хотел рассказать о каких-то челленджах в построении storage систем по типу S3. Обычно в таком духе я спрашиваю людей на собеседованиях, поэтому тут мысли вслух, любые совпадения случайны.

Всё понятно с корректностью -- храним метаданные транзакционно, чтобы ни дай бог ничего не потерять, Paxos нам в помощь, это много работы, но хотя бы понятно как сделать.

Диски хоть и дешёвые, но на масштабах S3 любая экономия байт уходит за миллионы/сотни миллионов долларов. S3 сжимает данные с помощью ZSTD https://news.ycombinator.com/item?id=32529412 и внутри скорее всего есть какой-нибудь Erasure Coding, чтобы не хранить дорогие копии.

Понятное дело, что когда вы загружаете данные, никто не будет сжимать или хранить в памяти весь файл, поэтому скорее всего данные как-то разбиваются на чанки по несколько мегабайт максимум, иначе было бы очень дорого поддерживать такую систему. Учитывая, что S3 спокойно утилизирует всю пропускную способность вашей сети, скорее всего эти чанки по несколько мегабайт независимы, то есть не надо знать результат декомпрессии предыдущего для следующего. Откуда дальше происходит несколько интересных размышлений:

* Есть свобода хранить чанки как угодно

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

* Упаковка также помогает процессам вида FSCK и garbage collection работать лучше и использовать меньше дисков, например, проверять на консистентность и удалять больше данных вместе, что снимает нагрузку

А дальше интересный вопрос, на который я и сам не знаю ответов: как лучше всего сделать упаковку? С одной стороны не хочется паковать блоки, которые должны быть скоро удалены с теми, кто никогда не должен удаляться, потому что иногда в упаковке шардов образуются дырки, с другой сделать что-то совсем простое не так тривиально. Без дырок не обойтись, но хотелось бы их поменьше. Я придумал только несколько небольших эвристик

* Хранить данные с похожим TTL в +-1 день вместе. Тем самым не образовывая много дырок

* Скорее всего некоторые пользователи или форматы файлов удаляются без TTL какими-нибудь человеческими усилиями. На них нужно писать предикторы с простыми линейными моделями

* Делать холодные данные холоднее, горячие горячее, например, выделить бюджет дисков, чтобы увеличивать размерность Erasure Coding и хранить ещё меньше байт для них.

Если кто-то знает литературу о упаковках и локальности в таких системах, напишите, я не нашёл.
👍83🔥1813💯3🤔2🥱1😭1
https://www.vldb.org/pvldb/vol16/p2132-afroozeh.pdf

Прочитал тут статью с VLDB про формат данных в базах данных, который даёт много идей на подумать. Даже есть репозиторий https://github.com/cwida/FastLanes

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

* SIMD friendly

* Поддерживает все виды несложного сжатия: parquet, delta encoding, RLE

Ну так как я что-то делал в этой области, статья написано хорошо! Упрощённо: предлагается хранить, например, не 64 битные числа, а каждый байт в 8 колонках. Для меньшего количества бит происходит более интересное чередование, но суть похожа, все это пакуется в 1024 битные регистры.

Чтобы поддержать такое сжатие и воспользоваться всеми преимуществами, авторы перебирали форматы и научились в целом находить Наименьшее Общее Кратное того, как биты должны быть расположены, чтобы все 3 формата сжатия удовлетворить. К сожалению, не обходится без большого количества табличных значений, что опять даёт возможность пользоваться не всем и видеть преимущества на больших данных, когда нужно обработать много памяти и заиспользовать всю её пропускную способность. В итоге получились регистры из 1024 бит, но с поддержкой всех SIMDов всех современных процессоров и неплохими бенчмарками.

Из интересных деталей про "НОК формата", авторы разбирают как они пытались уменьшить зависимость данных. В delta encoding (или префикс суммы) у вас каждое значение зависит от предыдущего. Но можно хранить 4 начальных значения и теперь суммировать 4 числа одновременно, что выдаёт баланс между пропускной способностью и уровнем сжатия. Такими transposed штуками и подбирали создание формата.

Себе взял на заметку, буду тестировать, но я минимально рад, что хотя бы нашёл время почитать статью.
👍50🔥168🤯3😱31👻1
Compressed pair

В стандартной библиотеке C++ множество контейнеров принимают allocator<A>, который по дефолту занимает 0 байт. Но в C++ не могут быть структуры (в отличие от C) с sizeof равным нулю. Значит элементами класса их не сделать бесплатно. В итоге приходилось использовать Empty Base Optimization, когда наследование от класса с нулевым размером оптимизируется в void.
Чтобы это как-то унифицировать, в libc++ сделали compressed_pair<A, B>, чтобы можно было писать члены класса и зафиксировать ABI. Делали A каким-то нужным полем, а B, например, аллокатором, тем самым sizeof сохранялся.

В C++20 добавили атрибут [[no_unique_address]], который запрещает адресоваться к структурам размера ноль, а если более точно, то при попытке так сделать даст какой-то адрес какого-то члена класса.

Спустя 4 года завезли в libc++ и заменили compressed_pair. Патч тащили 9 месяцев, потому что поломалось всё в дебагерах. Дотащили, дебаг символы в хромиум уменьшились на 5%, компиляция ускорилась на 1-1.5%, что не может не радовать.

https://github.com/llvm/llvm-project/pull/76756
👍90🤯17🔥138🍾6
January update (старею)

1. Мой (первый) стажёр из Яндекса опубликовал Perforator — сборщик профилей и перформанса на всём кластере. После 7 лет Сергей уже оч сильный инженер, конечно

https://github.com/yandex/perforator

https://habr.com/ru/companies/yandex/articles/875070/

В Гугле мы занимаемся именно этим (GWP), так что потихоньку мои ученики меня превосходят, а я старею.

Почитайте, хорошо всё сделано, но у меня, конечно, конфликт интересов :)

2. Закончил писать контест от телеграма по оптимизации валидации TON блоков. Суть была в том, чтобы надо было заооптимизировать код Коли Дурова, который они с командой написали в 2018-2019(?). Там около 40к строк кода, попросили заооптимизировать валидацию блоков, это где-то 10к строк кода. Читал я это дело несколько дней, но мультипоточные идеи там не супер хороши, потому что блоки валидировать надо последовательно и только лишь небольшие куски можно положить на потоки

Ну вообще всё было неплохо написано, и TON действительно неплохо сделан. Я нашёл 30% ускорения в среднем на одном ядре в целом по достаточно несложным ошибкам — что показывает что даже если у вас звёздная команда, нужен жёсткий тулинг, который ловит много проблем.

Может, я мало наооптимизировал и уже старею, ну результаты покажут. Я кайфанул, остальное становится менее важно.

3. Очень понравилась статья от Ашота про CPU порты и как можно ускорить сложение через инструкции fma (multiply add, когда multiply = 1.0), которые находятся на других портах на самых современных процессорах. Прекрасная идея! Сложно её обобщить, но знать о ней стоит :)

https://ashvardanian.com/posts/cpu-ports/
115👍43🔥42💘4❤‍🔥1👏1
1. Объявили результаты TON: дали серебро, оказался 6-7 по топу (Mindful Kitten), дали $5000. Я доволен, так как не упоролся, но и позанимался чем-то интересным.

2. Meta рассказала про свою версию GWP/Perforator -- профилирование всех датацентров.

So, the engineer typed an “&” after the auto keyword to indicate we want a reference instead of a copy. It was a one-character commit, which, after it was shipped to production, equated to an estimated 15,000 servers in capacity savings per year!


Немного смешно читать такой маркетинг, потому что давным давно такие штуки мы уже сделали в Google и автоматизировали по самое не хочу. В целом поздравляю, такие вещи позволяют держать CapEx в порядке и возможно помогут избежать хоть каких-то увольнений (a.k.a. могло быть и хуже).

3. https://dl.acm.org/doi/10.1145/3651890.3672262

В Google очень много данных надо копировать между датацентрами -- поисковый индекс, данные для обучения моделей и так далее. Наконец-то выложили ещё одну статью о достаточно большом и популярном внутри сервисе, как копировать данные между многими дц. Объёмы копирования до сих пор в моей голове не сильно укладываются: ~1 Eib (экзабайт) в день. Не помню статьей с таким масштабом. Несколько интересных поинтов про статью:

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

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

* "Fairness" среди одинаковых приоритетов, то есть копируется в среднем по-очередно у одного приоритета, но если углубляться, то там формула, о которой не рассказали

* Чексуммы, копии, Erasure codes, всё на месте. Битые данные возникают, ничего страшного. Но да, считать чексуммы на экзабайты в день тоже не самое дешёвое занятие. "we get 590 corruptions over 129EB for a rate of 4.55 corruptions per EB"

* Проводятся учения, чтобы найти пользователей, которые пользуются системой слишком много без выделенных на то ресурсов. "To stay ahead, Effingo team scheduled unavailability of best effort in three clusters for a period of around one day. As a result, four product teams detected that their production-critical systems unintentionally rely on best-effort; they obtained production quota"

Статья не описывает, почему, например, решили не делать torrent-like систему, но зато описывает, почему полностью централизованная система не сработала бы -- слишком уж большие объёмы.

В целом, в копировании данных мало можно соптимизировать не работая с клиентами -- тебя просят просто скопировать точно в нужных местах. Поэтому Effingo фокусируется на том, что может максимально -- оптимизиция стоимости пересылки этих данных так, чтобы они не мешали всем другим критичным сервисам гугла.
🔥123👍4715🎉5👏3😱3🤯2
Сегодня про закон Литтла.

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

Как теория очередей связана с нагрузкой? Закон Литтла, один из самых простых и легко понимаемых результатов теории очередей, гласит, что среднее количество клиентов в системе равно средней скорости прибытия, умноженной на среднее время пребывания в системе. В этом случае клиенты — это запросы в обработке, скорость прибытия — это объем трафика, представленного серверу, а время пребывания — это количество времени, необходимое для обработки каждого запроса. Закон доказывается не самым простым, но вполне понимаемым для студента образом. На удивление в курсах по компьютерным наукам эта тема практически не затрагивается, а вот в на курсах по физике, не так мало информации можно найти!

И о сколько интересных результатов вы можете получить с этим законом. Например, когда вы читаете с HDD диска, то вы сначала перемещайте головку диска где-то за 10ms, а потом читаете со 100 MB/s скоростью. Если вы хотите тратить меньше половины времени на seek, чем на чтение данных, то вам нужно иметь более (20 - 10) ms * 100 MB/s = 1 MB на чтение за раз. За пределами единичных множителей поверх этого числа вы получаете убывающую отдачу с точки зрения амортизации времени 10/(10 + X MB), потраченного в пустую на seek для чтения всех данных. Это полезно знать, потому что большие блоки не дают читать другим процессам, которым тоже нужны данные, а вот откуда и получаются примерные размеры блоков, какие мы должны или хотим читать за раз. Пару раз от людей из пространства слышал что-то в духе как "не заseekать диск до смерти", когда они обсуждали закон Литтла.

И то же самое можно применить в условии фикса багов/тикетов в команде. Количество текущих багов примерно равно среднему времени фикса помножить на скорость их пребывания.

В какой-то степени все этот закон понимают интуитивно в простых случаях -- скажем, у вас seek HDD диска занимает 10ms, то значит вы можете всего 100 IOPS сделать на нём (всего на HDD может быть 1 in-flight request), но теряются когда надо ответить на тот же вопрос про SSD.

Например, если SSD/NVME обрабатывает запросы в среднем за 20 микросекунд и делает это с 10M IOPS, то одновременно в среднем обрабатывается 200 запросов. Если подумать, выше этого немного тяжело уйти, надо уметь 200 запросов очень быстро класть в очередь (из разных потоков в идеале, так как переключение контекстов уже 1-2 микросекунды) для обработки и отправлять устройству. Поэтому любые оптимизации в драйверах, firmware, ядре и так далее сильно увеличивают цифры IOPS, особенно на больших квантилях. Ну и показывает, что скорее всего лимиты вашей системы ещё ой как недоутилизированы.
👍108🔥2218🤯3
Size based vector

https://discourse.llvm.org/t/adding-a-size-based-vector-to-libc-s-unstable-abi/86306

Мы тут в Гугле экспериментировали с тем как репрезентовать вектор. Существует два способа:

1. Указатель на начало, конец и указатель на конец вместимости

2. Или указатель на начало, размер и вместимость

Оба варианта имеют свои особенности и слабые места. Первый вариант плох тем, что когда вы хотите посчитать size(), то вы вычитаете два указателя: end - begin. Вычитание указателей в численном представлении эквивалентно формуле (end_as_num - begin_as_num) / sizeof(T), где T -- тип вектора. Вот это деление на константу порой выбешивает, например, когда sizeof(T) не является степенью двойки. Компилятору приходится это деление переводить в умножение и теперь когда вы вызываете size(), то у вас откуда-то страшные конструкции вида https://godbolt.org/z/zKGz7nEE6

Но первый вариант неплох, когда вы итерируетесь и надо просто сравнивать с концом. Почему? Во втором варианте вам надо при вызове .end() загружать два регистра -- начало и размер, чтобы сложить. В итоге у вас баланс между двумя опциями

.size() выливается в умножение при sizeof(T) не степень двойки

.end() загружает два регистра

Остальные операции чуть чуть поменяются, но в основном размен происходит у этих двух.

Оказалось, что .end() чаще вызывается один раз, а .size() намного чаще в том числе и внутри циклов, потому что... Ну потому что программистам удобнее работать с числами, а не указателями. Или по каким-то ещё причинам.

В итоге мы увидели улучшение перфа всего прода на 0.12% с особенно важными серверами с исправлениями на 0.5-0.6%, о чем и поделились в discourse.llvm. Понятное дело, что кто-то слишком сильно пользовался репрезентацией вектора, но мы всех их починили и выкатили. Теперь хотим выкатить и в unstable ABI в libcxx.

Почитайте ссылку, там больше всяких анализов, в том числе и размер кодгена, и всякой ещё статистики.
👍15324🔥12🤔4😢1
Trivially relocatable

В C++ есть большая группа людей (включая меня), которая любит брать оптимизации из C -- mem* функции возможно являются сильнейшим преимуществом C перед многими другими языками в перформансе. В C++ об этом думали и делали аттрибут std::trivially_copyable, который разрешает копировать как memcpy. Отлично работает на примитивных структурах и в целом C++ такой быстрый в том числе и из-за этого.

Но бывают и более интересные кейсы. Когда вы добавляете элементы в std::vector, рано или поздно вам надо будет реаллоцировать. Вы будете копировать числа/делать std::move объектов из предыдущего вектора. В ситуации с числами можно звать memcpy, они тривиальны, всё отлично. Но на самом деле звать memcpy можно и не только на тривиально копируемые типы, а, например, на unique_ptr<T>, или QString (который с ref count), или vector<int>!

Ведь копирование указателей, чисел вида размера/вместимости при тривиальном std::move ни к чему плохому не приведёт. К сожалению, по стандарту так нельзя. Когда вы начинаете делать mem* функции на типы, у которых примитивный std::move, то стандарт ничего для этого не приготовил и только говорит, что вы обязаны вызвать деструктор, и memcpy не позволяет начать жизнь более сложных объектов.

Поэтому в последние лет 6 идёт борьба за то, чтобы внести понятие тривиально релоцируемых типов -- у которых move оператор и move конструкторы устроены так, что это просто копирование по битам. Если быть точным, нужно ещё, чтобы деструктор не делал странных вещей, например, ничего не делал для пустых векторов, нулевых указателей и тд.

Без этого вставки в середину вектора, удаления из векторов долго не могли использовать memcpy тоже для таких типов. Существует ряд оптимизаций, который открывается из этого. В том числе interop с Rust станет слегка попроще :)

6 лет бились (первые года 4 не сильно, комитету в целом нравилось предложение), и таки уехало в феврале этого года в C++26.

Есть отличный блог из 5 небольших частей почитать о том, как это устроено в Qt.

Сам пропозал
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2786r13.html#abstract
🔥15830👍20🤔5🌭2😐2🙉2👏1
Latency profiler

Представьте ситуацию, вы пишете код на Python, вызываете посередине библиотеку, например, для inference, понимаете, что программа работает медленно. Собираете профиль с помощью стандартных утилит вроде perf, и в итоге видите следующую картину

90% Matmul
10% some Python

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

К счастью, не все так просто. Perf собирает события со всех потоков и CPU, которые суммируются. В итоге если вдруг умножение матриц было на многих ядрах, скажем, 8, то вы увидите в профиле, что вес matmul в 8 раз больше.

Недавно в perf добавили --latency, тулза, которая делит все события на количество активных CPU. После этого вы можете увидеть профиль в духе

40% matmul
60% some Python

Теперь оптимизировать питон есть смысл для задержки (latency), ведь профиль показывает, что из реального времени вы провели 40% в умножении матриц (может, на многих ядрах) и 60% в питоне (скорее всего в 1 ядре). Это часто бывает полезно в серверах, системах сборки, в тулзах командной строки.

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

https://www.phoronix.com/news/Linux-6.15-Perf-Tools-Latency
👍199🔥7928👀4🤡2🐳1🦄1