Программирование с учетом филиалов

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

Мои вопросы:

  1. Можно ли избежать неверных предсказаний ветвления, используя какой-либо метод программирования высокого уровня (например, без сборки)?

  2. Что мне следует иметь в виду, создавая дружественный к ветвям код на языке программирования высокого уровня (меня больше всего интересуют C и C ++)?

Приветствуются примеры кода и тесты.


person Paolo M    schedule 15.09.2015    source источник
comment
Поскольку предсказание ветвлений происходит только на машинном уровне, на самом деле нет смысла запрашивать его на уровне языка программирования высокого уровня. Компиляторы обычно содержат механизмы, зависящие от поставщика, для аннотирования условного выражения с ожидаемым результатом, но компилятор по-прежнему должен генерировать то, что, по его мнению, является лучшим машинным кодом (и это может быть изменено, например, с помощью оптимизаций на основе профиля или ограничений по пространству). В конечном счете, вам необходимо знать машину, если вам важны детали машины, и вам нужно понимать свои инструменты профилирования.   -  person Kerrek SB    schedule 15.09.2015
comment
Вы должны доверить это своему оптимизирующему компилятору. GCC предоставляет вам _ 1_   -  person Basile Starynkevitch    schedule 15.09.2015
comment
Сохранение списков отсортированными может помочь, так как это позволит коду, например if (x ‹10), придерживаться одного пути дольше.   -  person rhughes    schedule 15.09.2015
comment
Это зависит от компилятора. GCC, например, имеет _builtin_expect.   -  person David Schwartz    schedule 15.09.2015
comment
stackoverflow.com/questions/3702903/   -  person Tom    schedule 15.09.2015
comment
@iammilind Да, я читал эту ветку. Это очень интересно и дает некоторые подсказки о том, что я ищу, например, loop-interchange, сортировка данных, ...   -  person Paolo M    schedule 15.09.2015
comment
Есть такие вещи, как атрибуты функции noreturn, указывающие на то, что путь кода маловероятен, и компилятор должен оптимизировать код, чтобы следовать альтернативному пути.   -  person sharptooth    schedule 15.09.2015
comment
Стандартный способ AFAIK - это оптимизация под управлением профиля (en.wikipedia.org/wiki/Profile-guided_optimization). Например, GCC / ICC поддерживает его, и я ожидаю, что все другие основные компиляторы будут делать это. Он работает просто и элегантно: (а) компилируйте программу, используя аннотации, чтобы отслеживать часто выполняемые части кода; (б) запустить образец рабочей нагрузки; (c) перекомпилировать программу, используя собранную статистику, чтобы минимизировать ошибочные предсказания переходов.   -  person Radu    schedule 15.09.2015
comment
@KerrekSB Вы были очень ясны, спасибо. Вы бы сказали, что ответ на вопрос №1 - НЕТ, верно? А как насчет вопроса №2? Я имею в виду, что я могу сделать как программист при разработке и реализации приложения для написания дружественного к ветвям кода? В своем предыдущем комментарии я объясняю, что ищу.   -  person Paolo M    schedule 15.09.2015
comment
@rhughes Спасибо, мне нужны рекомендации, подобные этому.   -  person Paolo M    schedule 15.09.2015
comment
@PaoloM Нет проблем. Просто помните, что время, необходимое для фактической сортировки списка, может перевесить те преимущества, которые вы ищете.   -  person rhughes    schedule 15.09.2015
comment
@PaoloM: чтобы написать код, удобный для ветвей, вам нужно научиться использовать инструменты профилирования, а для этого вам нужно понимать их вывод, и я ожидаю, что в какой-то момент вам понадобится читать машинный код.   -  person Kerrek SB    schedule 15.09.2015
comment
Вам также следует прочитать Есть ли подсказка компилятора для GCC, которая заставляет предсказание ветвления всегда идти определенным путем? и все предостережения. которые изложены там в ответах.   -  person Shafik Yaghmour    schedule 15.09.2015
comment
Очень важно держать в поле зрения общую картину. Во-первых, профилируйте код и выясните, какие части стоит оптимизировать. Самым экстремальным примером из реальной жизни, над которым я работал, была программа из 250 000 строк, в которой более 90% вычислений выполнялось в одном цикле, который занимал всего 3 строки кода. Невозможно было исключить работу, проделанную в этом цикле. Оптимизация чего-либо в остальной части программы была бы пустой тратой усилий.   -  person alephzero    schedule 15.09.2015
comment
Что касается второго квартала (что я должен иметь в виду ...) почти ничего до (следуя комментарию @ alephzero и остерегайтесь преждевременной оптимизации Кнута) вы знать наверняка, что часть кода вызывает (несоразмерные) проблемы.   -  person TripeHound    schedule 15.09.2015
comment
В некоторых ситуациях вы можете переписать код с ветвями, чтобы они не содержали ветвей, что может быть выгодным, в зависимости от вашей ситуации. Есть несколько хороших статей об этом на Производительность сотовой связи   -  person mattnewport    schedule 16.09.2015


Ответы (8)


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

(*) Опытные программисты часто напоминают, что люди-программисты очень плохо умеют это предсказывать.

1- Можно ли избежать неверных предсказаний ветвления, используя какой-либо метод программирования высокого уровня (то есть без сборки)?

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

Некоторые компиляторы предоставляют расширение для предложения прогнозов вручную, например __builtin_expect в gcc . Вот вопрос о stackoverflow по этому поводу. Более того, некоторые компиляторы (например, gcc) поддерживают профилирование кода и автоматически определяют оптимальные прогнозы. Из-за (*) разумнее использовать профилирование, а не ручную работу.

2- Что мне нужно иметь в виду, чтобы создавать дружественный к ветвям код на языке программирования высокого уровня (меня больше всего интересуют C и C ++)?

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

Но что я могу сделать, если какой-то профилировщик (valgrind, VTune, ...) сообщает, что в строке n файла foo.cpp я получил штраф за предсказание ветвления?

Лундин дал очень дельный совет

  1. Измерьте для выяснения, имеет ли это значение.
  2. If it matters, then
    • Minimize the depth of dependency chains of your calculations. How to do that can be quite complicated and beyond my expertise and there's not much you can do without diving into assembly. What you can do in a high level language is to minimize the number of conditional checks (**). Otherwise you're at the mercy of compiler optimization. Avoiding deep dependency chains also allows more efficient use of out-of-order superscalar processors.
    • Сделайте ваши ветки стабильно предсказуемыми. Эффект от этого можно увидеть в этом stackoverflow вопрос. В вопросе есть цикл по массиву. Цикл содержит ветвь. Ветвь зависит от размера текущего элемента. Когда данные были отсортированы, можно было продемонстрировать, что цикл работает намного быстрее при компиляции с помощью определенного компилятора и запуске на конкретном процессоре. Конечно, хранение всех ваших данных в сортировке также будет стоить процессорного времени, возможно, больше, чем ошибочные предсказания ветвления, так что измерьте.
  3. Если проблема не исчезла, воспользуйтесь оптимизацией по профилю (если доступно).

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

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

person eerorika    schedule 15.09.2015
comment
Я считаю, что на вопрос 1 дан ответ, спасибо. Но что я могу сделать, если какой-то профилировщик (valgrind, VTune, ...) сообщает, что в строке n файла foo.cpp я получил штраф за предсказание ветвления? - person Paolo M; 15.09.2015
comment
@PaoloM Вы должны взглянуть на этот код и посмотреть, имеет ли вообще этот штраф какое-либо значение для производительности программы. Скорее всего, нет. В том редком случае, когда это так, вы просто попытаетесь переписать код, чтобы он содержал как можно меньше условных проверок. - person Lundin; 15.09.2015
comment
Даже в примечаниях gcc к __builtin_expect, которые я цитирую здесь, говорится, что вам следует использовать для этого фактические отзывы профиля (-fprofile -arcs), так как программисты, как известно, плохо предсказывают, как их программы на самом деле работают - person Shafik Yaghmour; 15.09.2015
comment
трансформируйте ваши циклы, например, разворачивая их - я уверен, что компилятор сделает это за вас ... - person John Dvorak; 15.09.2015
comment
@JanDvorak Да, если вы попросите его сделать это с соответствующими флагами оптимизации. Однако есть случаи, когда позволять компилятору разворачивать все ваши циклы (по усмотрению оптимизатора) нежелательно, и в этом случае вам придется вручную развернуть циклы, для которых это желательно. - person eerorika; 15.09.2015
comment
Еще одно интересное практическое правило предотвращения ветвлений можно найти в Руководство пользователя cachegrind: Также может быть полезно посмотреть на пропуски Bcm и BIM. В частности, промахи BIM часто вызываются операторами switch, и в некоторых случаях эти операторы switch могут быть заменены кодом, управляемым таблицами. - person Paolo M; 16.09.2015
comment
@PaoloM действительно, избавившись от ваших условных выражений (switch), вы полностью избавитесь от ветвей - и, следовательно, от прогнозирования ветвлений - в целом. - person eerorika; 16.09.2015
comment
У вас есть (*) в ответе, но я не могу найти отношения. И я хотел бы проголосовать за, но это удерживает меня от этого. - person gsamaras; 14.10.2015
comment
@gsamaras Первый (*) - это абзац, на который имеется ссылка, и на него будет сделана ссылка позже. Я запутанно поставил первый в конце абзаца и теперь переместил его на передний план. Надеюсь, это проясняет это. - person eerorika; 14.10.2015
comment
По большей части это неверно. Процессоры не предполагают оба пути. Цепочки зависимостей почти не имеют отношения к стоимости неверного прогнозирования. Спекуляции впереди примерно на 200 инструкций. Предсказание ветвлений является одним из основных факторов, влияющих на управляемость IPC (задержка памяти является ведущей). Плюс ко всему, с оптимизацией могут справиться только эксперты, эта фишка меня продолжает раздражать. - person Veedrac; 08.08.2018

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

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

Профилирование

Как уже было сказано, я не мастер компьютерной архитектуры, но я знаю, как профилировать код с помощью VTune и измерять такие вещи, как неверные предсказания ветвлений и промахи кеша, и делать это все время, находясь в критичной для производительности области. Это первое, на что вам следует обратить внимание, если вы не знаете, как это сделать (профилирование). Большинство этих горячих точек на микроуровне лучше всего обнаруживать задним числом, имея в руках профилировщик.

Удаление ветки

Многие люди дают отличные низкоуровневые советы о том, как улучшить предсказуемость ваших веток. Вы даже можете вручную попытаться помочь предсказателю ветвления в некоторых случаях, а также оптимизировать для статического предсказания ветвления (например, написание операторов if для проверки общих случаев). Здесь есть исчерпывающая статья о мельчайших подробностях от Intel: https://software.intel.com/en-us/articles/branch-and-loop-reorganization-to-prevent-mispredicts.

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

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

Пропуск мелкой / редкой работы

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

Примером этого является запоминание большой таблицы поиска, чтобы избежать многократного выполнения некоторых относительно дешевых вычислений, таких как использование таблицы поиска, охватывающей мегабайты, чтобы избежать повторного вызова cos и sin. Человеческому мозгу это кажется экономией работы, чтобы вычислить его один раз и сохранить, за исключением того, что загрузка памяти из этого гигантского LUT вниз по иерархии памяти и в регистр часто оказывается даже более дорогостоящей, чем вычисления, которые они предназначались. сохранить.

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

Эта наивная попытка разветвления в качестве оптимизации также применима даже к немного дорогой, но редкой работе. Возьмем этот пример C ++:

struct Foo
{
    ...
    Foo& operator=(const Foo& other)
    {
        // Avoid unnecessary self-assignment.
        if (this != &other)
        {
            ...
        }
        return *this;
    }
    ...
};

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

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

struct Foo
{
    ...
    Foo& operator=(const Foo& other)
    {
        // Don't check for self-assignment.
        ...
        return *this;
    }
    ...
};

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

Наивная попытка создать небольшой вектор

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

char str[256];
// do stuff with 'str'

... и, естественно, поскольку у нас была довольно обширная база пользователей, некоторые редкие пользователи в конечном итоге вводили имя для материала в нашем программном обеспечении, длина которого превышала 255 символов, и переполняли буфер, что приводило к сбою. Наша команда углублялась в C ++ и начала портировать множество этих исходных файлов на C ++ и заменять такой код следующим:

std::string str = ...;
// do stuff with 'str'

... который без особых усилий устранил переполнение буфера. Однако, по крайней мере тогда, контейнеры типа std::string и std::vector были структурами, распределенными по куче (свободному хранилищу), и мы обнаружили, что торгуем правильностью / безопасностью ради эффективности. Некоторые из этих замененных областей были критичными к производительности (вызываемые в тесных циклах), и хотя мы устранили множество отчетов об ошибках с помощью этих массовых замен, пользователи начали замечать замедление.

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

Моя первая наивная попытка была примерно такой (значительно упрощенной: на самом деле использовалось новое размещение и т. Д., И это была полностью соответствующая стандарту последовательность). Он включает использование буфера фиксированного размера (размер, указанный во время компиляции) для общего случая и динамически выделяемого, если размер превышает эту емкость.

template <class T, int N>
class SmallVector
{
public:
    ...
    T& operator[](int n)
    {
        return num < N ? buf[n]: ptr[n];
    }
    ...
private:
    T buf[N];
    T* ptr;
};

Эта попытка окончилась неудачей. Хотя он не платил за создание кучи / свободного хранилища, разветвление в operator[] сделало его еще хуже, чем в std::string и std::vector<char>, и отображалось как горячая точка профилирования вместо malloc (наша реализация поставщика std::allocator и operator new использовала malloc под капотом). Тогда у меня возникла идея просто присвоить ptr buf в конструкторе. Теперь ptr указывает на buf даже в общем случае, а теперь operator[] может быть реализовано следующим образом:

T& operator[](int n)
{
    return ptr[n];
}

... и с этим простым удалением ветвей наши горячие точки исчезли. Теперь у нас был универсальный, совместимый со стандартами контейнер, который мы могли использовать, который был примерно таким же быстрым, как и прежнее решение с фиксированным буфером в стиле C (разница лишь в одном дополнительном указателе и еще нескольких инструкциях в конструкторе), но может справиться с теми редкими сценариями, когда размер должен быть больше N. Теперь мы используем это даже больше, чем std::vector (но только потому, что наши варианты использования отдают предпочтение кучке крохотных, временных, смежных контейнеров с произвольным доступом). И чтобы сделать это быстро, нужно просто удалить ветку в operator[].

Распространенный / редкий случай перекоса

Одна из вещей, которую вы поняли после профилирования и оптимизации в течение многих лет, заключается в том, что не существует такой вещи, как "абсолютно быстрый везде" код. Большая часть процесса оптимизации заключается в обмене неэффективности на большую эффективность. Пользователи могут воспринимать ваш код как абсолютно быстрый и везде, но это происходит из-за разумных компромиссов, когда оптимизации согласовываются с общим случаем (общий случай совпадает с реалистичными сценариями на стороне пользователя и исходит из горячих точек указал профилировщик, измеряющий эти распространенные сценарии).

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

Обработка исключений без затрат

Примером перекоса типичного / редкого случая является техника обработки исключений, используемая во многих современных компиляторах. Они применяют EH с нулевой стоимостью, что на самом деле не является «нулевой стоимостью» повсеместно. В случае возникновения исключения они теперь работают медленнее, чем когда-либо прежде. Тем не менее, в случае, когда исключение не генерируется, они теперь быстрее, чем когда-либо прежде, и часто быстрее в успешных сценариях, чем такой код:

if (!try_something())
    return error;
if (!try_something_else())
    return error;
...

Когда вместо этого мы используем EH с нулевой стоимостью и избегаем проверки и распространения ошибок вручную, в неисключительных случаях все идет еще быстрее, чем в приведенном выше стиле кода. Грубо говоря, это связано с уменьшением ветвления. Однако взамен при возникновении исключения должно произойти что-то гораздо более дорогое. Тем не менее, это несоответствие между обычным и редким случаем имеет тенденцию помогать в реальных сценариях. Нас не столько заботит скорость неудачной загрузки файла (редкий случай), сколько его успешная загрузка (частый случай), и именно поэтому многие современные компиляторы C ++ реализуют EH с нулевой стоимостью. Это опять же в интересах искажения общего случая и редкого случая, отодвигая их дальше друг от друга с точки зрения производительности.

Виртуальная рассылка и однородность

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

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

for each entity in world:
    entity.do_something() // virtual call

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

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

for each human in world.humans():
    human.do_something()
for each orc in world.orcs():
    orc.do_something()
for each creature in world.creatures():
    creature.do_something()

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

Оптимизация, ориентированная на данные

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

Здесь может помочь изучение дизайна, ориентированного на данные. Я обнаружил, что некоторые из наиболее полезных знаний, касающихся оптимизации, получены при изучении оптимизации памяти в контексте проектирования, ориентированного на данные. Дизайн, ориентированный на данные, как правило, подчеркивает меньшее количество абстракций (если они есть) и более громоздкие высокоуровневые интерфейсы, которые обрабатывают большие блоки данных. По своей природе такие конструкции имеют тенденцию уменьшать количество разрозненных ветвлений и скачков в коде за счет более зацикленного кода, обрабатывающего большие фрагменты однородных данных.

Часто помогает сосредоточиться на более быстром использовании данных, даже если ваша цель - уменьшить количество ошибочных прогнозов ветвлений. Например, я обнаружил некоторые большие выгоды от SIMD вне филиалов, но мышление по-прежнему было в духе более быстрого потребления данных (что и произошло, и благодаря некоторой помощи отсюда, SO, как Гарольд).

TL; DR

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

person Community    schedule 14.12.2015

Ядро Linux определяет макросы likely и unlikely на основе встроенных __builtin_expect gcc:

    #define likely(x)   __builtin_expect(!!(x), 1)
    #define unlikely(x) __builtin_expect(!!(x), 0)

(Определения макросов в include/linux/compiler.h см. здесь)

Вы можете использовать их как:

if (likely(a > 42)) {
    /* ... */
} 

or

if (unlikely(ret_value < 0)) {
    /* ... */
}
person ouah    schedule 15.09.2015
comment
Не знал, что ядро ​​определяет макросы :) - person BitTickler; 21.09.2020

В общем, рекомендуется поддерживать горячие внутренние циклы в соответствии с наиболее часто встречающимися размерами кеша. То есть, если ваша программа обрабатывает данные в кусках, скажем, менее 32 Кбайт за раз и выполняет с ними приличный объем работы, то вы эффективно используете кеш L1.

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

Это важно, потому что одна из причин, по которой процессоры имеют предсказание ветвления, в первую очередь, состоит в том, чтобы иметь возможность предварительно выбрать операнды для следующей инструкции. Последствия для производительности неправильного предсказания ветвления можно уменьшить, упорядочив код таким образом, чтобы были хорошие шансы, что следующие данные будут поступать из кеша L1, независимо от того, какая ветвь будет выбрана. Хотя это и не идеальная стратегия, размер кэша L1, кажется, повсеместно застрял на 32 или 64 КБ; это почти постоянная вещь во всей отрасли. По общему признанию, кодирование таким способом не всегда просто, и полагаться на оптимизацию, управляемую профилем, и т. Д., Как рекомендуют другие, вероятно, самый простой путь вперед.

Независимо от чего-либо еще, возникнет ли проблема с неверным предсказанием ветвления, зависит от размеров кеш-памяти ЦП, того, что еще работает на машине, какова пропускная способность / задержка основной памяти и т. Д.

person bazza    schedule 15.09.2015

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

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

person MSalters    schedule 15.09.2015
comment
Если у ошибки есть какая-либо неоспоримая вероятность возникновения, этот совет совершенно неверен: возникновение исключительной ситуации связано с огромными потерями производительности. Никогда не вводите исключения в поток программы, если вы заботитесь о производительности. - person cmaster - reinstate monica; 06.11.2017
comment
@cmaster: Даже если вероятность исключения значима и вы заботитесь о производительности в неисключительном случае, вы часто не заботитесь о производительности в исключительном случае. Пример: компиляция кода. Ошибки компиляции, безусловно, могут произойти, и время сборки для больших проектов, безусловно, вызывает беспокойство. Но накладные расходы, связанные с исключением, совершенно ничтожны по сравнению со временем, потраченным человеком на рассмотрение ошибки. - person MSalters; 06.11.2017
comment
Мои рассуждения просты: время, потерянное из-за исключений, exceptionFrequency*handlingTime. handlingTime огромен, поэтому exceptionFrequency должен исчезнуть, чтобы продукт был маленьким. Таким образом, если ваше исключение генерируется только один раз в секунду, продолжайте и используйте его (если вы не возражаете против исключений в вашем коде). Если есть вероятность, что ваше исключение генерируется более тысячи раз в секунду, это быстро приведет к серьезному снижению производительности. Однако условия ошибки имеют тенденцию проявляться практически во всех функциях и запускаться регулярно. Не для чего использовать исключения. - person cmaster - reinstate monica; 06.11.2017
comment
@cmaster: Дело в том (поскольку речь идет о программировании с учетом ветвлений), что исключения экономят время в порядке (1-exceptionChance)*overheadOfErrorHandlingInNormalCase. Если вы вызываете функцию тысячу раз в секунду и у вас есть возвращаемое значение ошибки, ее необходимо проверять тысячу раз в секунду. Если эта ошибка является исключением, компилятор может оптимизировать сценарий без ошибок. Если ошибка закодирована как отрицательное целое число, компилятор не имеет этого руководства. - person MSalters; 06.11.2017
comment
И за то время, когда вы можете выбросить / перехватить одно исключение, вы можете легко проверить тысячу ошибок. - person cmaster - reinstate monica; 06.11.2017
comment
@cmaster: Яблоки и апельсины. В конце концов, речь идет об управлении потоком, и в момент, когда вы поймали исключение, вы уже прошли точку ветвления в потоке (и вы находитесь на медленном ветвлении). Простая проверка кода ошибки еще не является операцией управления потоком, а разветвление обходится дорого. (тем более, что этот тип ветвей загрязняет кеш предсказателя ветвлений - исключения обычно не зависят от предсказанных ветвей, поскольку компилятор может использовать ветвь, которую не принимают, например, код операции x86 0x2E). - person MSalters; 06.11.2017
comment
Нет, никаких яблок и апельсинов: если я знаю, что могу выполнить так много ветвей за время выполнения одного броска / улова, я могу взвесить затраты. Я знаю, лучше ли просто проверить условия ошибки или вместо этого использовать исключение. Я хочу сказать, что выполнение throw / catch намного дороже, чем if / else, что вам нужно удалить много выполняемых операторов if / else из вашего горячего пути выполнения, чтобы компенсировать один throw / поймать. В среднем, ЦП должен видеть менее одного броска из нескольких сотен, если он не видит, чтобы делать исключения быстрее. - person cmaster - reinstate monica; 06.11.2017
comment
Просто запустил простой тест: в моей системе выброс исключения на один уровень функции в 483 раза дороже, чем возврат кода ошибки из функции вместо этого ... - person cmaster - reinstate monica; 06.11.2017

1- Можно ли избежать неверных предсказаний ветвления, используя какой-либо метод программирования высокого уровня (то есть без сборки)?

Избегать? Возможно нет. Уменьшать? Конечно...

2- Что мне нужно иметь в виду, чтобы создавать дружественный к ветвям код на языке программирования высокого уровня (меня больше всего интересуют C и C ++)?

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

person autistic    schedule 15.09.2015

Чтобы ответить на ваши вопросы, позвольте мне объяснить, как работает прогнозирование ветвлений.

Прежде всего, существует штраф за переход, когда процессор правильно предсказывает предпринятые переходы. Если процессор предсказывает переход как принятый, то он должен знать цель предсказанного перехода, поскольку поток выполнения будет продолжен с этого адреса. Предполагая, что целевой адрес ветвления уже сохранен в Branch Target Buffer (BTB), он должен получить новые инструкции с адреса, найденного в BTB. Таким образом, вы все равно тратите несколько тактовых циклов, даже если ветвление правильно спрогнозировано.
Поскольку BTB имеет ассоциативную структуру кеша, целевой адрес может отсутствовать, и, следовательно, может быть потрачено больше тактовых циклов.

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

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

Можно ли избежать неправильного предсказания ветвления с помощью какой-либо техники программирования высокого уровня (т.е. без сборки)?

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

Что мне нужно иметь в виду, чтобы создавать дружественный к ветвям код на языке программирования высокого уровня (меня больше всего интересуют C и C ++)?

Если есть возможность, по возможности удалите ветки. Если это не тот случай при написании операторов if-else или switch, сначала проверьте наиболее распространенные случаи, чтобы убедиться, что ветви с наибольшей вероятностью не будут взяты. Попробуйте использовать функцию __builtin_expect(condition, 1), чтобы заставить компилятор создать условие, которое будет считаться невыполненным.

person Root G    schedule 06.03.2017

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

См. Флаг оптимизации gcc -O3 делает код медленнее, чем -O2 для случая, когда gcc -O3 преобразует if() код без ветвей в случае, когда это очень предсказуемо, что делает его медленнее.

Иногда вы уверены, что условие непредсказуемо (например, в алгоритме сортировки или бинарном поиске). Или вы больше заботитесь о том, чтобы наихудший случай был не в 10 раз медленнее, чем о том, чтобы быстрый случай был в 1,5 раза быстрее.


Некоторые идиомы с большей вероятностью будут скомпилированы в форму без ветвей (например, инструкция условного перемещения cmov x86).

x = x>limit ? limit : x;   // likely to compile branchless

if (x>limit) x=limit;      // less likely to compile branchless, but still can

Первый способ всегда записывает в x, а второй не изменяет x в одной из ветвей. Похоже, это причина того, что некоторые компиляторы обычно создают ветвь вместо cmov для версии if. Это применимо, даже когда x является локальной int переменной, которая уже находится в регистре, поэтому "запись" не требует сохранения в памяти, а просто меняет значение в регистре.

Компиляторы по-прежнему могут делать все, что захотят, но я обнаружил, что эта разница в идиоме может иметь значение. В зависимости от того, что вы тестируете, это иногда лучше помочь маске компилятора и оператору AND, чем использовать старый добрый cmov. Я сделал это в том ответе, потому что знал, что у компилятора будет то, что нужно для генерации маски с помощью одного инструкция (и увидев, как это сделал clang).

ЗАДАЧИ: примеры на http://gcc.godbolt.org/

person Peter Cordes    schedule 27.12.2015
comment
В примере кода первым ":" в первой строке должен быть "?". - person hexcoder; 18.06.2018