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

#c #compiler-optimization

#c #компилятор-оптимизация

Вопрос:

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

 void loadValuesFromDisk(const bool scaleEveryValueByTwo)
{
    std::vector<MyValueType> xs;
    while(fileHasNewValues())
    {
        auto x = loadNextValue();
        if (scaleEveryValueByTwo)
        {
            x *= 2;
        }
        xs.push_back(x);
    }
}
 

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

 void loadValuesFromDisk(const bool scaleEveryValueByTwo)
{
    std::vector<MyValueType> xs;
    while(fileHasNewValues())
    {
        auto x = loadNextValue();
        xs.push_back(x);
    }

    if (scaleEveryValueByTwo)
    {
        for(auto amp;x : xs)
        {
            x *= 2;
        }
    }
}
 

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

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

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

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

3. @NathanOliver правильно… Компилятор, скорее всего, выполнит эту оптимизацию, если это хорошая идея . Задавать такого рода вопросы вне контекста результата профилирования почти наверняка является преждевременной оптимизацией (т. Е. Неправильным вопросом).

Ответ №1:

Ранее в компиляторе MSVC использовался /Og (глобальная оптимизация), которые теперь включены по умолчанию. Я предполагаю, что другие компиляторы также делают это.

Чтобы узнать, как выполняется оптимизация цикла, перейдите по ссылке ниже и найдите «Оптимизация цикла»

https://docs.microsoft.com/en-us/cpp/build/reference/og-global-optimizations?view=vs-2019

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

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

1. Это навело меня на правильный путь, у других компиляторов, похоже, есть похожие опции, например -fmove-loop-invariants , и несколько более агрессивных для gcc .

Ответ №2:

Вы можете создать scaleEveryValueByTwo параметр шаблона, чтобы быть уверенным, что условие вычисляется только один раз. В C 17 вы можете использовать if constexpr следующее

 template <bool scaleEveryValueByTwo>
void loadValuesFromDisk()
{
    std::vector<MyValueType> xs;
    while(fileHasNewValues())
    {
        auto x = loadNextValue();
        if constexpr (scaleEveryValueByTwo)
        {
            x *= 2;
        }
        xs.push_back(x);
    }
}
 

Если у вас еще нет C 17, приведенный выше код можно получить, например, с помощью вспомогательной шаблонной функции multiply следующим образом

 template <bool activate>
void multiply(decltype(loadNextValue())amp; x);

template <>
void multiply<true>(decltype(loadNextValue())amp; x) { x *= 2; }

template <>
void multiply<false>(decltype(loadNextValue())amp; x) { }

template <bool scaleEveryValueByTwo>
void loadValuesFromDisk()
{
    std::vector<MyValueType> xs;
    while(fileHasNewValues())
    {
        auto x = loadNextValue();
        multiply<scaleEveryValueByTwo>(x);
        xs.push_back(x);
    }
}
 

(Примечание: я использую decltype , потому что не знаю, что loadNextValue() возвращает ваша процедура.)

Затем вы вызываете либо loadValuesFromDisk<true>() или loadValuesFromDisk<false>() . Если scaleEveryValueByTwo известно только во время выполнения, вы можете перейти к соответствующей функции:

 void loadValuesFromDisk(bool const scaleEveryValueByTwo)
{
    if (scaleEveryValueByTwo)
        loadValuesFromDisk<true>();
    else
        loadValuesFromDisk<false>();
}
 

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

1. Это if constexpr не требуется для случая, предшествующего C 17. if (/* pure constexpr constant */) { компилятор определенно оптимизирует.