Как стать автором
Обновить

Комментарии 91

А если debug сделать не переменной, а константой? По идее компилятор тогда просто выбросит всю ветку if(DEBUG) на этапе компиляции, если значение DEBUG = false.
Понятно, что зависит от компилятора, но чтобы компилятор не понимал таких очевидных вещей, он должен быть совсем примитивным.

А лучше вообще использовать условную компиляцию, тогда в код ни одной лишней команды не попадет.

Только для того чтобы включить флажок вместо перезапуска приложения с флагом --debug его нужно пересобирать

Или использовать язык с JIT.

Бранчпредиктор работает не хуже

В некоторых случаях — хуже. Шипилёв приводил в своих докладах примеры, где опознание JITʼом ситуации в рантайме помогает выкидывать заметные неоптимизируемые куски (типа лукапа адреса виртуальной функции из памяти там, где согласно типу объекта может быть только одна такая функция). Предикторы такое не ловят.

Буквально тут же в статье пишут, что несрабатывающее никогда условие — бесплатно. А if (debug) при запуске с release именно такое условие.


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

А if (debug) при запуске с release именно такое условие.

Зависит от компилятора
gcc.godbolt.org/z/rxPqdhP6r
clang и gcc делают по разному.

Ну они же не телепаты. Если им передать нужную инфу


void test(int dbg)
{
  if (dbg) [[unlikely]] {
      printf("blah");
  }

  printf("foo");
}

То компилировать начинают одинаково

Ну они же не телепаты

Да, поэтому по умолчанию делают разное. ЧТД.

И как часто вы в коде видите такие атрибуты?
Особенно в том, что написан до их стандартизации?

Ну я на расте пишу, и там постоянно их вижу. Вроде эта статья про бранчпредиктор в целом, а не только про плюсы.


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

НЛО прилетело и опубликовало эту надпись здесь
Буквально тут же в статье пишут, что несрабатывающее никогда условие — бесплатно. А if (debug) при запуске с release именно такое условие.

Бесплатных условий не бывает. Занято место в коде, тратится в кэшах, переменную надо вычитать из памяти, чтобы проверить значение, и всё такое. Кэш branch predictorʼа тоже ограниченный, данные из него вымываются, и если снова попасть на это условие, то первая сработка может быть неправильной. Если кода много и условия проверки if(debug) раскиданы по нему разнообразно и несистемно, то проверяться они будут вразнобой. Всё вместе даёт удорожание, которое на некоторых шаблонах применения весьма заметно.
(Кстати, поэтому некоторые предпочитают отладку делать не условиями в коде, а через внешний отладчик и расстановку точек останова с действиями по срабатыванию. Запуск с отладкой в этом случае может быть дороже, но без отладки — дешевле.)


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

Даже при теле в пару мелких операций — проверка будет тратить ресурс.

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


Я понимаю, что в теории это сжирает ресурсы. А на практике совершенно нерелевантно — есть куда более жрущие куски, чем забивание бранч предиктора.

А на практике совершенно нерелевантно — есть куда более жрущие куски, чем забивание бранч предиктора.

Практика практике рознь.
Вот есть такой интересный зверёк kdb+, пришлось с ним поработать. Он весь оптимизирован на максимум скорости работы в RAM, хранение своих таблиц по колонкам, максимум скорости выжимаемой из SSE/AVX, и так далее.
У него целые числа имеют свои inf и nan, для каждого из типовых размеров (1, 2, 4, 8 байт), и это отрабатывается много где… но, например, если у вас значение 0Nh (short nan) и вы вызвали конверсию в int, у вас не будет 0Ni (int nan), будет -32768. Почему, спросите? Потому что в скалярном варианте — лишний бранч, а в векторном — куча специфических сравнений и слияний по маскам. Кому нужно — тот явно напишет проверку.
И таких решений там десятки. Даже для тех, кто хочет, более медленные, но корректные с точки зрения математики типов решения не предоставлены — потому что будут занимать драгоценное место в кэшах. Ядро движка ужато по максимуму.
Зато, когда встаёт вопрос "а что самое быстрое", боюсь, конкурентов нет (хотелось бы, но никак не вырастут).


Понятно, что пример маргинальный, и большинство кода вместо этого будут стоять на I/O или хотя бы на ожидании RAM, отрабатывать 100500 крайних случаев со своей спецификацией… но я о том, что наглядные примеры пользы от branchless+SIMD находятся совсем недалеко.

У него целые числа имеют свои inf и nan, для каждого из типовых размеров (1, 2, 4, 8 байт), и это отрабатывается много где
А разве такие вещи не удобнее писать на Ada, например?

Нет, потому что его язык очень хорошо оптимизирован, украшенно говоря, для работы брокера, который не умеет программировать, но каждый лишний символ в "программе" в шелле обходится в 10$.

У бранчпредиктора ограниченный размер кэша. А JIT-рантайм отоптимизировал hot path и забыл про него, пошёл другие места оптимизировать.

Насколько исчерпание этого кэша реально затормозит программу — вопрос открытый. Ведь tight loop должен кэшироваться целиком, а остальной код не так сильно влияет.

Лично мне больше нравилось процедурную переменную использовать, вместо if-then устанавливать её в указатель на реальный логгер или на заглушку. А если уж патчить, то не реальный код, а VMT конкретного объекта.

Но… при этом в AOT-языках сохраняются накладные расходы на подготовку аргументов (даже если они не будут использованы), а в JIT-языках и без этого отключённый код должен в итоге быть убран.
Тогда не получится менять уровни логгирования без перезапуска приложения, а при перезапуске придётся снова ждать, пока проблема воспроизведётся.
НЛО прилетело и опубликовало эту надпись здесь
Чтобы собрать информацию о проблеме без остановки сервиса.
А в чем проблема остановки сервиса если можно делать rolling update?
Да даже зачем останавливать если можно запустить дополнительный инстанс с отладочной информацией и часть трафика (нужного вам для отладки) перенаправить на него?
Процитирую себя же на пару комментариев выше:

> а при перезапуске придётся снова ждать, пока проблема воспроизведётся.

Хм. Погодите. Вы про сценарий когда логи всегда включены и всё логируют? Или про сценарий когда логи выключены конфигурацией и можно в рантайме их включить?

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

Если про первое - то, это постоянная посадка производительности (логирование не бесплатное) и повышенное потребление ресурсов (cpu, сети, дисков).

Если про второе — то вам точно также нужно будет ждать когда проблема воспроизведется.

Допустим, проблема перевела инстанс в некоторое невалидное состояние (не настолько невалидное, чтобы он упал, но ведёт себя теперь странно). Если его перезапустить, то поведение нормализуется, но мы так и не узнаем подробности об ошибке, а снова она появится неизвестно когда. А так включаем логгирование и можем детально исследовать странные ответы на запросы. Так что подход может быть применим для багов, выходящих за пределы одного запроса.

Это вопрос компромисса ведь.

На сколько часто вы встречаетесь с таким типом ошибок? На моей практике их были счётные единицы.

Если же вы их встречаете часто и можете позволить себе общее падение производительности, повышенные требования к железу, то почему бы и нет?

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

Возможно это игровой бек и у него от перекрестий условий где-то сорвало крышу.

Но такой подход всегда и везде, на мой взгляд конечно, не очень оправдан.

На сколько часто вы встречаетесь с таким типом ошибок? На моей практике их были счётные единицы.


Проблема в том, что эти считанные еденицы НЕВЕРОЯТНО трудно поймать и пофиксить. У нас был баг, который мог появится только спустя неделю аптайма. Т.е. на проде — регулярно, у девелоперов — никогда. И вот как с таким жить?

Видимо в таких случаях и включить отладочную информацию и ждать. А пока нет такого бага - не включать :)

НЛО прилетело и опубликовало эту надпись здесь
«ИТ для бизнеса, а не бизнес для ИТ»
А хотелось бы получить дебажную информацию от текущего инстанса.

Если это statefull сервис, то возможно иногда такое и требуется.

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

Я не против того, чтобы поднимать сервисы с отладочной информацией. Если бизнес готов это оплачивать, то почему нет?

Просто это в большинстве случаев избыточно и не приносит существенной пользы.

У любого сервиса в коде можно написать:
if (debug) log("");
можно. но вот на моей практике были случаи когда такой if (debug) съедал до 70% процессорного времени от всего исполнения бизнес-операции. И это не смотря на то, что сам debug был отключён.
Разумеется не одно условие, а все такие условия которые попадались по пути исполнения бизнес-логики. Для сервисов у которых время отклика критично — это проблема. Поэтому приходится прибегать к более хитрым приёмам. Например, как описанные в статье.
Если уже такты приходится считать — конечно надо выкидывать из кода весь балласт.

А если нет такой задачи и при этом хочется без перезапуска, а тем более без перекомпиляции получить диагностический вывод программы, то решение if (debug) — вполне рабочее.
У ARM, насколько я помню, есть условное исполнение инструкций. То есть, можно обойтись без ветвлений, а процессор будет просто пропускать инструкции в зависимости от состояния флагов. Не знаю, используется ли эта фича в современных компиляторах.
То есть, можно обойтись без ветвлений, а процессор будет просто пропускать инструкции в зависимости от состояния флагов.

Имхо, все техники важны и каждой технике оптимизаций своё место. Описанная вами имеет проблему в том, что половина инструкций (соответствующая невыполняемой ветке) обязательно отбрасывается (т.е. в конвейере будут пузыри, ведь на этапе компиляции неизвестно значение условия). В то же время для BTB будут отброшены только те, которые попали на случай неправильно предсказанного перехода. На циклах (без if-ов в теле цикла) BTB видится существенно эффективнее условного исполнения.
С другой стороны, условные инструкции повышают плотность кода, а значит, у цикла выше шанс уместиться в I-кэше.

Не знаю как на arm, но на x86 cmov зачастую дороже, чем предсказанный переход: выключается out-of-order execution, из-за того, что результат исполнения команды на execution unit становится зависим от состояния флагов, в то время как предсказанные условные переходы + mov вообще не задействуют execution unit, там просто отработает переименование регистров.

У меня создалось впечатление, что ВТВ у М1 прям в кэше и находится. Добавили дополнительное поле в кэш и все. Ведь, по сути своей, у ВТВ и кэш очень похожи и на их объединении можно профит получить.
Не очень понятно зачем вообще нужен branch prediction для безусловных операчий jmp, call/ret?
Ведь они безусловные, мы точно занаем что надо прыгать.
Ведь они безусловные, мы точно занаем что надо прыгать.

Не знаем. Предсказание ветвлений работает на ранних стадиях конвейера, где инструкция перехода ещё не декодирована. Она даже может быть не прочитана из памяти.
Поначалу тоже показалось неочевидным, но в начале статьи хорошо расписано: предсказатель работает до декодера инструкций. Чтобы не тратить время на декодирование инструкций, идущих за jmp — нужно предсказать этот переход, и подать в декодер инструкции уже по адресу перехода.
Всё равно не очень понятно. Ведь чтобы узнать адрес перехода, нужно декодировать инструкцию целиком.
Классический бранч предиктор, насколько я знаю, держал для каждой ячейки таблицы (адрес бранча поксореный на что-то там) два бита для машины состояний — strong/weak taken/not taken и предсказывал именно, произойдёт условный переход, или нет, чтобы начать out of order execution именно правильной ветки.
Тут, видимо, про другой механизм, который для каждого бранча кеширует именно адрес назначения.
Выигрыш от такого, конечно будет даже для безусловных, а для условных ещё больше, но вот места будет занимать больше, и при коллизиях и миспредикшенах будет возможно больнее.
Непонятно, это дополнительный механизм, или старый заменили на такой вот новый когда-то.
Всё равно не очень понятно. Ведь чтобы узнать адрес перехода, нужно декодировать инструкцию целиком.

Вы вообще читали статью?

два бита для машины состояний

Современные предсказатели — сложные многоуровневые устройства.
Если вы посмотрите на блок-схему BPU в статье — можете обнаружить bimodal predictor, действующий согласно описанному вами принципу.
Видимо, в этом месте читал по-диагонали, спасибо.
Даже безусловные jmp и call бывают косвенными, и тогда важно предсказать, куда именно будет переход. Ret всегда косвенный, так что для него это ещё актуальнее.

Для ret наверно оптимизацию проводят, генерируя при call сразу 2 записи в btb: для call и ret. Думаю, такое можно обнаружить в тестах из данной статьи, выходя из функции по jmp.

Каким образом при выполнении call они могут узнать адреса всех ret в вызванной процедуре, чтобы занести эти адреса в BTB?

Да, действительно ошибся

чтобы можно было подкачать и спекулятивно начать исполнять код из вероятного места прыжка, одновременно проверяя правильность предсказания, и в случае чего, то, что спекулятивно наисполнялось — выкинуть. Короче, чтоб конвейер в большинстве случаев не простаивал.
Я понимаю, что EPYC не выстрелил, но насколько активно выполняется переползание с BPU на оптимизации компилятора? Насколько я помню, компиляторы достаточно эффективно обрубают недостижимые ветви и спрямляют поток исполнения.
Эти две задачи не зависят одна от другой: скажем, в коде будут процедуры специально для AMD и отдельно для Intel, тогда компилятор не может выбросить ни те ни другие, но выполняться на каждой машине будут либо те либо другие.
С BPU на оптимизации компилятора можно эффективно пересесть, если у вас JIT. Тот может сделать branch prediction за процессора, наиболее вероятное место перехода заинлайнить под if и этот if спрямить например (спекулятивным исполнением), что openjdk например и делает. Это дает возможность относительно эффективно прыгнуть за косвенный вызов (по указателю, или через таблицу виртуальных функций, что одно и то же). Если компилятор статический, то он такого делать не умеет, и косвенные вызовы автоматом стопают конвейер и становятся дорогими.
Profile-guided optimization не даёт статическому компилятору аналогичной информации?

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

Спасибо за ссылку, утянул к себе в закладки.
В дополнение к статье по ссылке могу добавить, что jmp на холодный бранч — это он в интерпертатор сразу прыгает (который, к слову, в хотспоте так себе сделан)
да, дает. Но с ограничениями. Например, профиль мы строим в одних условиях, а коду пришлось внезапно работать в других.
Во всей статье упоминается про «в вашем критичном коде не должно быть больше 4096 ветвлений». Но сама статья показывает, что как только BTB забивается, то всё — производительность падает. Т.е. это даже не во всей программе надо делать меньше 4096 переходов, а чтобы к моменту выполнения вашего кода, столько прыжков не должно было встретиться.

BTB так или иначе переполняется, хотя бы работой ОС. И вероятнее всего, вытеснение выполняется каким-то ужасно кондовым алгоритмом, вроде FIFO, как следствие, в случае критичного цикла первый проход по нему "прогревает" BTB, последующие проходы используют всё ещё не вытесненные из BTB предсказания переходов, если их мало. А после выхода из критичного сегмента или скажем context switch'a его забивает другими данными, но нам уже пофигу.

Статья рассказывает о граничных характеристиках BTB. А вот задача разработчика и компилятора обеспечить чтобы максимальное количество критического к производительности кода выполнялось в рамках этих граничных условий. Тогда будет бенефит. А начальное состояние BTB не влияет на результат, если только не остались данные "обучения" от предыдущего выполнения этого же кода.

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

Вот не лень-же было кому-то столько строк писать. Хотя есть подозрение, что это сделал автомат. Современные умные помощники с перекаченным AI — такие задания разгадывают за пару строк.
Смотрим что выдаёт функция: строку фиксированной длинной, значит все данные можно загнать в один непрерывный массив без нулей. Из 257 условных переходов останется ноль штук. Только прямое чтение из массива и добавление нуля. Строка полностью помещается в регистр, так-что проблем ноль.
НЛО прилетело и опубликовало эту надпись здесь
Самое главное, что использование массива (можно даже простой массив строк сделать, а не извращаться с фиксированной длинной строки) значительно повысит читаемость кода. А в текущей реализации уже допустили ошибку — два раза сравнили с единицей. Так что тут далеко не только про оптимизацию, но и просто про читаемость. Если программисту приходит в голову написать несколько сотен одинаковых if, ему стоит хорошенько задуматься, не подойдёт ли в этом месте лучше массив или даже HashMap (если индексы содержат дырки). Производительность производительностью, а кому-то потом может потребоваться этот код читать и модифицировать.

Да уж. А то, чтобы, чтобы написать тесты к этому безобразию, надо опять что ли всю портянку копировать?

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

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

Но я ожидал ответов на вопросы типа →
Будет ли код вида

run_main_function()
if (debug) {
 log(...)
}


значительно эффективнее, чем

if (debug) {
 run_main_function()
 log(...)
} else {
 run_main_function()
}


где, по факту выполняется то же самое, но в любом случае проходит через ветвление? Или обе записи равнозначны по производительности, потому что практически всегда выполняется только else?
Или, например, что будет быстрее (или одинаково?)

if ((A == 1 and B == 2) or (A == 1 and B == 3)) {
 run()
}


или
if (A == 1) {
 if (B == 2 or B == 3) {
  run()
 }
}


Или с этими if вообще не стоит заморачиваться, ибо компилятор сделает все как надо, по-пацански? А как быть с некомпилируемыми языками типо javascript?

Ну и вообще, как эффективнее писать ветвления?

Код, критичный к производительности должен быть меньше 16 КиБ

Что это за сферические 16 КиБ в вакууме? Ну вот, предположим, есть у меня run.py размером в 1KиБ, а в нем импорты сторонних библиотек на XМиБ. Такой код не подходит? Если не подходит, то совет в 99% не имеет смысла в реальной жизни. Любая современная библиотека раздута до гораздо бОльших размеров, так что уложить в лимиты можно только если что учебный код для реферата.

В общем, информация получена, а что с этим делать не совсем непонятно (ну, по-крайней мере, деревенскому необразованному парню — мне).
Ну вот, предположим, есть у меня run.py размером в 1KиБ

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

где, по факту выполняется то же самое, но в любом случае проходит через ветвление?

В обоих приведённых вами случаях неизбежен проход через ветвление (проверка debug и условный переход).

А как быть с некомпилируемыми языками типо javascript?

Если вам важно время выполнения с точностью до такта, то вам не стоит писать на JS.

Ну и вообще, как эффективнее писать ветвления?

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

Что это за сферические 16 КиБ в вакууме?

Речь, грубо говоря, о теле цикла вместе со всеми функциями, вызываемыми изнутри этого цикла. К размеру программы целиком, и тем более библиотек целиком, эти рекомендации отношения не имеют.
Ну вот, предположим, есть у меня run.py размером в 1KиБ, а в нем импорты сторонних библиотек на XМиБ.
1 киБ питоновского кода — это очень далеко от 1 кБ машинного кода, потому что он даже не компилируемый.
1 КБ питоновского кода вместе с выполняющим его ядром Python вполне уместятся в 16 КБ, и действительно, вопрос лишь в том, какие внешние функции этот 1 КБ питоновского кода вызывает.
Ну вот, предположим, есть у меня run.py размером в 1KиБ, а в нем импорты сторонних библиотек на XМиБ.
Если у вас есть run.py, то дальше уже неважно какого он размера и чего он там импортирует.

Ваш К.О.

Любая современная библиотека раздута до гораздо бОльших размеров, так что уложить в лимиты можно только если что учебный код для реферата.
Далеко не любая библиотека написана без игнорирования железа и во многиг библиотеках даже в гигабайты есть горячие циклы размером куда как меньше 16 килобайт.

Если вы такой код не пишите, то вам, соотвественно, обсуждаемая статья и не нужна.
Насколько я знаю, конкретно предсказание не улучшит никак, может лишь повлиять на исполняемый код — насколько я знаю, компилятор сделает такой условный jump, который не выполняется для likely-случая, и сразу после него поставит likely-ветвь выполнения. Таким образом, обычно переход не будет выполняться, и мы вроде как должны получить те самые «jne not-taken» тайминги с одного из графиков. Но вообще именно разницы между вариантом с likely/unlikely и без может и не быть, так как компилятор сам не дурак и вполне может генерировать идентичный код без них.
Но вообще именно разницы между вариантом с likely/unlikely и без может и не быть, так как компилятор сам не дурак и вполне может генерировать идентичный код без них.
Только при наличии PGO.

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

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

P.S. Не так сложно, на самом деле, правильно написать [[likely]] или [[unlikely]] при первоначальном написании кода. Но вот при рефакторинге или изменении техзаданий они часто оказываются неверными и после этого не ускоряют, а замедляют код.
Likely/unlikey вообще никак к PGO не относятся. И я писал именно о сравнении кода с этими атрибутами и без — он может быть идентичным. Например, MSVC, насколько я знаю, всегда располагает if-ветку выше else — строите условие таким образом, что if-ветка является likely, и никакие атрибуты не нужны. С unlikely условием без else чуть сложнее — вроде как мне помогала изоляция ветки в функцию/лямбду.
Ну и по угадыванию — не согласен, человек разрабатывает алгоритм, и он в первую очередь сам понимает, какая ветка когда будет выполняться; а вот компилятор об алгоритме знает сильно меньше и действительно пытается угадать. Так что в очевидных случаях ставить эти атрибуты можно и нужно, а в неочевидных — да, лучше вообще их не ставить, так как даже если угадаешь — выигрыш будет незначительным.
Likely/unlikey вообще никак к PGO не относятся.
Как не относятся? likely/unlikely указывают на то, какой код “горячий”, а какой “холодный”. Если PGO покрывает код достаточно хорошо, то компилятор просто это знает и никакие likely/unlikely не нужны.

Например, MSVC, насколько я знаю, всегда располагает if-ветку выше else — строите условие таким образом, что if-ветка является likely, и никакие атрибуты не нужны.
Если вы используете MSVC, то вначале нужно перейти на нормальный компилятор (clang, например, как Google сделал), а потом уже начинать заботиться об микрооптимизациях. Уже хотя бы тот факт, что в MSVC нет поддержки ассемблера для x64 уже ставит крест на всех долях процента, которые вы можете выиграть с помощью микрооптимизаций класса likely/unlikely.

Так что в очевидных случаях ставить эти атрибуты можно и нужно, а в неочевидных — да, лучше вообще их не ставить, так как даже если угадаешь — выигрыш будет незначительным.
Ну вот почитайте как с этим борются разработчики ядра. А там и код, вроде, не идиоты пишут, и каждый патч проходит через несколько ревьюеров. И всё равно портачат.
Как не относятся?
PGO — это оптимизация на основе тестовых запусков, likely/unlikely — оптимизация на основе знаний программиста. Из общего тут только оптимизация.
Если вы используете MSVC, то вначале нужно перейти на нормальный компилятор (clang, например, как Google сделал), а потом уже начинать заботиться об микрооптимизациях. Уже хотя бы тот факт, что в MSVC нет поддержки ассемблера для x64 уже ставит крест на всех долях процента, которые вы можете выиграть с помощью микрооптимизаций класса likely/unlikely.
Громкое заявленьице, хорошо бы оно было нормально аргументировано. По Вашей ссылке пишут, что производительность Chrome с MSVC и Clang примерно одинакова; ассемблерные вставки в обычных ситуациях не нужны, их отсутствие в MSVC конечно удручает, но гораздо больше успокаивает тем, что всякие ассемблерные умники не испортят ими код. К тому же, не поддерживаются именно вставки, линковку с asm файлами никто не отменял.
там и код, вроде, не идиоты пишут, и каждый патч проходит через несколько ревьюеров. И всё равно портачат.
Steven Rostedt has been looking at the problem using the annotated branch profiler and found ten places «that really do not need to have an annotated branch, or the annotation is simply the opposite of what it should be».
10 мест на все ядро? Мда, похоже на проблему тысячелетия.
10 мест на все ядро? Мда, похоже на проблему тысячелетия
А вы обратили внимание на то, что это регулряно проводимая процедура и у них для неё инструментация имеется? Подумайте — были бы все эти заморочки нужны, если бы разработчики могли, как вы говорите, просто правильно указать все эти likely/unlikely.

По Вашей ссылке пишут, что производительность Chrome с MSVC и Clang примерно одинакова
Вообще-то там это пишут только про вариант с PGO. А мы, вроде как likely/unlikely обсуждаем.

PGO — это оптимизация на основе тестовых запусков, likely/unlikely — оптимизация на основе знаний программиста. Из общего тут только оптимизация.
Согласно вашей логике сравнивать время поездки на поезде и на самолёте некорректно, потому что один летает, другой ездит.

PGO и likely/unlikely решают одну и ту же задачу на основе разных подходов.

И MSVC (без PGO) делает это настолько плохо (вы собственно сами описали как), что likely/unlikely ему помогают как мёртвому припарки. Да, собственно, вот:
int foo(int x) {
    if (x == 0) return 0;
    return 1000/x;
}
Функция буквально в три строки — и сразу видно что MSVC в принципе неспособен там нормальный код сгенерить. А Clang и GCC делают это по умолчанию.

А если вы используете PGO, то likely/unlikely просто не нужны.
Подумайте — были бы все эти заморочки нужны, если бы разработчики могли, как вы говорите, просто правильно указать все эти likely/unlikely.
Люди могут просто правильно работать с памятью, зачем managed-платформы/семантика владения как в Rust?
Люди могут просто не допускать логических ошибок, зачем тестирование?
Продолжать можно до бесконечности.
Вообще-то там это пишут только про вариант с PGO.
А про вариант без PGO там ни слова. Да и вообще там ни слова про то, что MSVC как-то хуже Clang'а, просто им Clang по инструментарию большие импонирует. Так что Вы пока что наедине со своим MSVC-хейтом.
Согласно вашей логике сравнивать время поездки на поезде и на самолёте некорректно, потому что один летает, другой ездит.
Да, поезд и самолет решают одну и ту же задачу. Но как они связанны между собой, кроме как тем, что и то и то — транспорт?
И MSVC (без PGO) делает это настолько плохо (вы собственно сами описали как), что likely/unlikely ему помогают как мёртвому припарки.
Во-первых, ну не выполняет MSVC такие оптимизации — поменяйте ветки местами и забудьте. Во-вторых, Ваш оптимальный Clang'овский код протухнет как только я решу вызывать эту функцию исключительно от нулевого аргумента. Так что если уж считать циклы, то все равно придется каждый такой момент самому продумать.
Еще можете в примере поменять int на int64_t и прочувствовать необъятную гениальность Clang'а.
Еще можете в примере поменять int на int64_t и прочувствовать необъятную гениальность Clang'а.
Заменил. Clang просёк, что тут можно использовать вместо дорогой инструкции 64-битного деления более дешёвую 32-битную. Круто. Не ожидал.
Насколько эта оптимизация полезна — сложный вопрос, при -Os он её не делает.

Во-вторых, Ваш оптимальный Clang'овский код протухнет как только я решу вызывать эту функцию исключительно от нулевого аргумента.
А вот как раз если у вас такая противоестественная программа, что в ней исключительный случай случается чаще, чем неисключительный — то вы и можете подправить код с помощью likely.

А про вариант без PGO там ни слова.
Знаете, по моему вы уже доказали, что вся дискуссия вам нужна только чтобы подтведить известную, как вам кажется, истину. Потому вы действуете только и исключительно пытаясь натянуть сову на глобус и доказать вашу правоту. И даже когда вас ткнули носом в наделанную вами лужу — это ни на что не влияет. Да, там есть про вариант без PGO: If you don’t use LTCG and PGO, it’s possible that Clang might create faster code.

И связано это как раз именно в то, что:
Во-первых, ну не выполняет MSVC такие оптимизации — поменяйте ветки местами и забудьте.
И…

Я вас правильно понял: я должен писать код не так, чтобы его было удобнее читать, а так, чтобы компилятору было удобнее его компилировать? Причём даже не используя предназначенные для этого аннотации, а выворачивая весь код “наизнанку”? А чо сразу на ассеблер не перейти? Там можно ещё лучший код с такими подходами написать…

Люди могут просто правильно работать с памятью, зачем managed-платформы/семантика владения как в Rust?
Люди могут просто не допускать логических ошибок, зачем тестирование?
Продолжать можно до бесконечности.
Ну дык да. Спасибо. Только это ж довод в пользу использования компилятора, который не требует, в большинстве случаев, задумываться над всеми likely/unlikely и позволяющий, в случае где это важно, обойтиси минимальными изменениями года. То есть не MSVC. Разве нет?

Это в MSVC компилятор не пытается переходы оттимизировать (без PGO) и не реагирует на likely/unlikely. GCC и Clang себя нормально ведут.

Так что Вы пока что наедине со своим MSVC-хейтом.
То есть приведя доводы в пользу отказа от него вы этого даже не заметили? Сильно.

Да, поезд и самолет решают одну и ту же задачу. Но как они связанны между собой, кроме как тем, что и то и то — транспорт?
Они связны тем, что оба являются валидными способами добраться из точки A в точку B. А PGO является валидным способом управления ветвлениями и инлайнингом, конкурирующим с likely/unlikely.

Вот только в случае с MSVC у вас есть только PGO (извините но трюки с лямбдами превращающие код чёрт-знает-во-что я всерьёз рассматривать не хочу), а в случае с более-менее вменяемыми компиляторами есть inline, likely/unlikely, почетки cold/hot (правда это уже не стандарт) и прочее.
Знаете, по моему вы уже доказали, что вся дискуссия вам нужна только чтобы подтведить известную, как вам кажется, истину.
Я дискутирую из-за того, что Вы пришли и облили грязью вполне нормальный и достаточно широко используемый компилятор. Причем по сути отсутствие автоматического жонглирования ветками условий является единственным высказанным Вами валидным аргументом, да и то это решаемая проблема, хоть решение порой и ведет к усложнению кода. И я ведь не говорил, что мол MSVC всем голова, а Clang выкинуть на помойку — нет, это Вы пришли и затеяли холивар.
А свои галлюцинации про лужи, мушкетеров и что там у Вас еще — лучше оставьте при себе. Если же они обостряются, советую жаловаться специалисту, а не интернет-форуму.

В статье очень хорошо освещено поведение при количестве переходов 256-8192, когда самое интересное конечно было бы при количестве переходов 16-256, что более вероятно для критических мест.


И ещё осталось непонятно, что у m1 с верно предсказанными условными переходами, не требующими перехода.

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

На момент перевода была, поэтому и переехала в перевод.

Исправил.

В оригинальной статье автор добавил еще P.S и рассказал там про уязвимость Spectre v2, которая как раз эксплуатирует branch prediction

Уместней было бы с другими ARM сравнивать, а так - продакт плэйсмент М1 народу с х86

Замечание 2. Условные переходы, которые никогда не выполняются, практически бесплатны. По крайней мере на этом процессоре.

Я когда-то тестировал php, может на Феноме, лет 10 назад
Так было наоборот. Код с условиями, которые обычно выполняются, был быстрее

Автор - молодец, статья в духе старого-доброго хабра. Чувствуется, что в свое время он много БЭВМ и ассемблера хлебнул

Зарегистрируйтесь на Хабре, чтобы оставить комментарий