Почему компиляторы не оптимизируют тривиальные указатели на функции-оболочки? - PullRequest
0 голосов
/ 08 февраля 2019

Рассмотрим следующий фрагмент кода

#include <vector>
#include <cstdlib>

void __attribute__ ((noinline)) calculate1(double& a, int x) { a += x; };
void __attribute__ ((noinline)) calculate2(double& a, int x) { a *= x; };
void wrapper1(double& a, int x) { calculate1(a, x); } 
void wrapper2(double& a, int x) { calculate2(a, x); } 

typedef void (*Func)(double&, int);

int main()
{
    std::vector<std::pair<double, Func>> pairs = {
        std::make_pair(0, (rand() % 2 ? &wrapper1 : &wrapper2)),
        std::make_pair(0, (rand() % 2 ? &wrapper1 : &wrapper2)),
    };

    for (auto& [a, wrapper] : pairs)
        (*wrapper)(a, 5);

    return pairs[0].first + pairs[1].first;
}

При оптимизации -O3 последние версии gcc и clang не оптимизируют указатели на оболочки для указателей на базовые функции.См. Сборку здесь в строке 22:

mov     ebp, OFFSET FLAT:wrapper2(double&, int)   # tmp118,

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

Обратите внимание, что я специально попросил не встроенные функции calculate для иллюстрации;выполнение этого без noinline приводит к другой разновидности неоптимизации, когда компилятор сгенерирует две идентичные функции, которые будут вызываться указателем (поэтому все равно не будет оптимизироваться, просто по-другому).

Что яздесь не хватает?Есть ли какой-нибудь способ не дать компилятору вручную подключить правильные функции (без упаковщиков)?

Редактировать 1. Следуя предложениям в комментариях, вот разборка со всеми объявленными статическими функциями, с одинаковым результатом (call + jmp вместо call).

Edit 2. Гораздо более простой пример того же шаблона:

#include <vector>
#include <cstdlib>

typedef void (*Func)(double&, int);

static void __attribute__ ((noinline)) calculate(double& a, int x) { a += x; };
static void wrapper(double& a, int x) { calculate(a, x); } 

int main() {
    double a = 5.0;
    Func f;
    if (rand() % 2)
        f = &wrapper; // f = &calculate;
    else
        f = &wrapper;
    f(a, 0); 
    return 0;
}

gcc 8.2 успешно оптимизирует этот код, выбрасывая указатель на обертку и сохраняя &calculate непосредственно на своем месте (https://gcc.godbolt.org/z/nMIBeo). Однако изменяя строку в соответствии с комментарием (то есть выполняячасть той же оптимизации вручную) ломает магию и приводит к бессмысленным jmp.

Ответы [ 2 ]

0 голосов
/ 08 февраля 2019

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

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

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

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

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

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

0 голосов
/ 08 февраля 2019

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

Я также предполагаю, что вы предполагаете, что компилятор может попытаться выполнить некоторый статический анализ иопределить, что значения указателей функций в векторе никогда не сравниваются на равенство с другими указателями функций, и фактически ни одна из других операций, выполняемых над элементами вектора, не приведет к изменению наблюдаемого поведения;и поэтому в этой точной программе он может хранить &calculate1 вместо этого.

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

...