Должен ли я использовать, если маловероятно для жестких ошибок?

Я часто нахожу, что пишу код, который выглядит примерно так:

if(a == nullptr) throw std::runtime_error("error at " __FILE__ ":" S__LINE__);

Должен ли я использовать ошибки с if unlikely?

if unlikely(a == nullptr) throw std::runtime_error("error at " __FILE__ ":" S__LINE__);

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

Ответы

Ответ 1

Должен ли я использовать "если маловероятно" для жестких сбоев?

В таких случаях я предпочел бы переместить код, который выдает автономную функцию extern, которая помечена как noreturn. Таким образом, ваш фактический код не "загрязнен" большим количеством кода, связанного с исключениями (или любого другого вашего "жесткого" кода). В отличие от принятого ответа, вам не нужно указывать его как cold, но вам действительно нужно noreturn, чтобы заставить компилятор не пытаться генерировать код для сохранения регистров или любого другого состояния и, по сути, предполагать, что после того, путь назад.

Например, если вы пишете код следующим образом:

#include <stdexcept>

#define _STR(x) #x
#define STR(x) _STR(x)

void test(const char* a)
{
    if(a == nullptr)
        throw std::runtime_error("error at " __FILE__ ":" STR(__LINE__));
}

компилятор будет генерировать множество инструкций, которые касаются построения и исключения этого исключения. Вы также вводите зависимость от std::runtime_error. Посмотрите, как сгенерированный код будет выглядеть как, если в вашей функции test есть только три проверки:

введите описание изображения здесь

Первое улучшение: переместить его в автономную функцию:

void my_runtime_error(const char* message);

#define _STR(x) #x
#define STR(x) _STR(x)

void test(const char* a)
{
    if (a == nullptr)
        my_runtime_error("error at " __FILE__ ":" STR(__LINE__));
}

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

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

#if defined(_MSC_VER)
#define NORETURN __declspec(noreturn)
#else
#define NORETURN __attribute__((__noreturn__))
#endif

void NORETURN my_runtime_error(const char* message);
...

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

введите описание изображения здесь

Как вы можете видеть, компилятор не должен сохранять регистры перед вызовом my_runtime_error.

Я также предлагаю не конкатенировать строки ошибок с __FILE__ и __LINE__ в монолитные строки сообщений об ошибках. Передайте их как автономные параметры и просто создайте макрос, который их передает!

void NORETURN my_runtime_error(const char* message, const char* file, int line);
#define MY_ERROR(msg) my_runtime_error(msg, __FILE__, __LINE__)

void test(const char* a)
{
    if (a == nullptr)
        MY_ERROR("error");
    if (a[0] == 'a')
        MY_ERROR("first letter is 'a'");
    if (a[0] == 'b')
        MY_ERROR("first letter is 'b'");
}

Может показаться, что на каждый вызов my_runtime_error (еще 2 команды в случае сборки x64) генерируется больше кода, но общий размер на самом деле меньше, так как сохраненный размер в постоянных строках больше, чем размер дополнительного кода.

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

#include <math.h>

#if defined(_MSC_VER)
#define NORETURN __declspec(noreturn)
#else
#define NORETURN __attribute__((noreturn))
#endif

void NORETURN my_runtime_error(const char* message, const char* file, int line);
#define MY_ERROR(msg) my_runtime_error(msg, __FILE__, __LINE__)

double test(double x)
{
    int i = floor(x);
    if (i < 10)
        MY_ERROR("error!");
    return 1.0*sqrt(i);
}

Сгенерированная сборка: введите описание изображения здесь

Попробуйте удалить noreturn или измените __attribute__((noreturn)) на __attribute__((cold)), и вы увидите полностью другую сгенерированную сборку! введите описание изображения здесь

В качестве последней точки (что очевидно, ИМО и опущено). Вам необходимо определить my_runtime_error в некотором cpp файле. Поскольку это будет только одна копия, вы можете поместить любой код в эту функцию.

void NORETURN my_runtime_error(const char* message, const char* file, int line)
{
    // you can log the message over network,
    // save it to a file and finally you can throw it an error:
    std::string msg = message;
    msg += " at ";
    msg += file;
    msg += ":";
    msg += std::to_string(line);
    throw std::runtime_error(msg);
}

Еще один момент: clang фактически признает, что этот тип функции выиграет от noreturn и предупреждает об этом, если -Wmissing-noreturn предупреждение:

предупреждение: функция 'my_runtime_error' может быть объявлена ​​с атрибутом 'noreturn' [-Wmissing-noreturn] {^

Ответ 2

Да, вы можете это сделать. Но еще лучше переместить throw в отдельную функцию и пометить ее __attribute__((cold, noreturn)). Это избавит вас от необходимости говорить unlikely() на каждом сайте вызова и может улучшить генерацию кода, перемещая логику исключения, полностью за пределами счастливого пути, улучшая эффективность кэша кэша и расширяя возможности.

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

Ответ 3

Это зависит.

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

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

UPDATE:

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

Это действительно очень широкая и сложная тема. То, что сделает компилятор, зависит от конкретного компилятора, его бэкэнд (целевой архитектуры) и параметров компиляции. Опять же, для x86 здесь следующее правило (взятое из Справочное руководство по оптимизации архитектур Intel® 64 и IA-32):

Правило сборки/компилятора. Правило 3. (М-воздействие, общая общность). Упорядочить код, который будет соответствовать алгоритм предсказания статической ветки: сделать провальный код, следующий за условной ветвью, следующим образом: вероятная цель для ветки с прямой целью, и сделать код падения после условного ветвь - маловероятная цель для ветки с обратной целью.

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

Ответ 4

Так как вы "врезаетесь", я бы пошел с

#include <cassert>

...
assert(a != nullptr);

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