Функция, оптимизированная для постоянной времени компиляции

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

float calcLength(const float *v, int size, int inc) {
    float l = 0;

    for (int i=0; i<size*inc; i += inc) {
        l += v[i]*v[i];
    }
    return sqrt(l);
}

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

Итак, у меня было бы что-то вроде этого:

template <int C>
struct Constant {
    static constexpr int value() {
        return C;
    }
};

struct Var {
    int v;

    constexpr Var(int p_v) : v(p_v) { }

    constexpr int value() const {
        return v;
    }
};

template <typename INC>
float calcLength(const float *v, int size, INC inc) {
        float l = 0;

        for (int i=0; i<size*inc.value(); i += inc.value()) {
            l += v[i]*v[i];
        }
        return sqrt(l);
    }
}

Итак, это можно использовать:

calcLength(v, size, Constant<1>()); // inc is a compile-time constant 1 here, calcLength can be vectorized

или

int inc = <some_value>;
calcLength(v, size, Var(inc)); // inc is a non-compile-time constant here, less possibilities of compiler optimization

Мой вопрос в том, можно ли каким-то образом сохранить исходный интерфейс и автоматически вставлять Constant/Var, в зависимости от типа (константа времени компиляции или нет) inc?

calcLength(v, size, 1); // this should end up calcLength(v, size, Constant<1>());
calcLength(v, size, inc); // this should end up calcLength(v, size, Var(int));

Примечание: это простой пример. В моей реальной проблеме у меня есть несколько функций, таких как calcLength, и они большие, я не хочу, чтобы компилятор их встраивал.


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

  • алгоритм указывается один раз (скорее всего в шаблонной функции)
  • если я укажу 1 как inc, будет создана специальная функция, и код, скорее всего, будет векторизован
  • если inc не является константой времени компиляции, вызывается общая функция
  • в противном случае (константа времени компиляции, отличная от 1): не имеет значения, какая функция вызывается

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

NathanOliver 19.03.2019 20:40

@NathanOliver: насколько я понимаю, это то, что я делаю в вопросе. С одним отличием: чтобы разрешить оба вида inc, у него есть параметр шаблона типа. Но результат тот же.

geza 19.03.2019 20:43

Вы ищете если constexpr ?

Jesper Juhl 19.03.2019 20:45

Я имею в виду такую ​​функцию, как template <std::size_t inc> float calcLength(const float *v, int size) { use inc here as a compile time value }.

NathanOliver 19.03.2019 20:45

@NathanOliver: ваше предложение имеет тот же результат, что и мое решение, если Constant<X> используется как inc.

geza 19.03.2019 20:46

Есть ли у вас какие-либо тесты, подтверждающие вашу конъюнктуру о «лучшей оптимизации»? Я могу изобразить гипотетические случаи, если inc % 8 == 0 или inc % 16 == 0, но не уверен, что векторизация будет намного лучше.

Dmitry 19.03.2019 20:48

@Dmitry: в моем случае, если inc известно во время компиляции, это будет 1 99% времени. И, конечно же, его можно оптимизировать намного лучше. У него огромная разница в скорости.

geza 19.03.2019 20:50

@geza Я думаю, что «у вас есть тесты» означает «опубликуйте свой тест, чтобы я мог убедиться, что мое решение работает».

anatolyg 19.03.2019 22:55

@anatolyg: у меня есть тесты, но не для этого простого случая. Но на самом деле этот вопрос не нуждается в эталоне. Это больше языковой вопрос. По сути, я хотел бы иметь функцию, которая может быть автоматически скомпилирована для константы времени компиляции. Мой код работает, но мне не нравится ручная спецификация Constant/Var. Я бы хотел, чтобы это было автоматически.

geza 19.03.2019 23:19

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

aschepler 19.03.2019 23:41

@aschepler: я упомянул в вопросе, что у меня есть несколько функций, и они огромны. Я абсолютно уверен, что компилятор не будет их встраивать из-за их размера. Мне нужно использовать какую-то функцию forceinline. Но я не хочу, потому что размер скомпилированного кода будет намного больше. Было бы ошибкой встраивать эти функции в любую текущую архитектуру.

geza 20.03.2019 00:18

@JesperJuhl: я не знаю, как я могу использовать if constexpr в своей проблеме, так что, вероятно, нет.

geza 20.03.2019 00:20

Но если inc не является константой времени компиляции, а имеет значение 1, какую функцию следует вызывать? Для вас нормально, если она называется Constant<1> версией?

max66 20.03.2019 02:10

@ max66: в основном это никогда не бывает. Но я думаю, что понимаю, почему вы об этом спрашиваете: я мог бы добавить небольшую встроенную функцию-оболочку, которая проверяет 1. Мне не очень нравится это решение, потому что оно добавляет ненужное if для случая константы времени не компиляции. Если это единственное решение, я буду продолжать использовать Constant/Var.

geza 20.03.2019 11:02
std::integral_constant, но, похоже, это не сработало: godbolt.org/z/YuSK0L
Mooing Duck 12.04.2019 19:49

Теперь, когда я включил оптимизацию, обе версии вычислялись во время компиляции: godbolt.org/z/-s1xNS

Mooing Duck 12.04.2019 19:55

Если вы не слишком возражаете против переносимости, __builtin_constant_p — отличный инструмент для такого рода оптимизации.

Marc Glisse 12.04.2019 20:06

Если вы используете GCC, как предложил @MarcGlisse, __builtin_constant_p будет работать, но если в этом примере все еще условия времени выполнения...

Hiroki 12.04.2019 20:41
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
18
305
3

Ответы 3

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

Если параметр должен является параметром функции, и вы не хотите менять способ его вызова в двух случаях, то единственный рычаг, который у вас есть, — это тип параметра: ваши предложения относительно Constant<1>() и Var(inc) довольно хороши. в этом отношении.

Если целью здесь является просто оптимизация, а не использование в контексте времени компиляции, вы можете дать компилятору подсказки о своих намерениях:

static float calcLength_inner(const float *v, int size, int inc) {
    float l = 0;

    for (int i=0; i<size*inc; i += inc) {
        l += v[i]*v[i];
    }
    return sqrt(l);
}

float calcLength(const float *v, int size, int inc) {
    if (inc == 1) {
        return calcLength_inner(v, size, inc);  // compiler knows inc == 1 here, and will optimize
    }
    else {
        return calcLength_inner(v, size, inc);
    }
}

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

Это трюк C (и широко используется внутри numpy), но вы можете написать простую оболочку, чтобы упростить ее использование в С++:

// give the compiler a hint that it can optimize `f` with knowledge of `cond`
template<typename Func>
auto optimize_for(bool cond, Func&& f) {
    if (cond) {
        return std::forward<Func>(f)();
    }
    else {
        return std::forward<Func>(f)();
    }
}

float calcLength(const float *v, int size, int inc) {
    return optimize_for(inc == 1, [&]{
        float l = 0;
        for (int i=0; i<size*inc; i += inc) {
            l += v[i]*v[i];
        }
        return sqrt(l);
    });
}

Хороший улов на отсутствующем возврате, исправлено

Eric 21.09.2019 21:31

Добавлена ​​ссылка на Godbolt

Eric 21.09.2019 22:20

Вариант 1: Доверьтесь компилятору (иначе ничего не делайте)

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

Компиляторы могут создавать так называемые «клоны функций», которые делают то, что вы хотите. Функция клонирования — это копия функции, используемой для распространения констант, то есть результирующая сборка функции, вызываемой с постоянными аргументами. Я нашел мало документации об этой функции, поэтому вам решать, хотите ли вы полагаться на нее.

Компилятор может полностью встроить эту функцию, потенциально делая вашу проблему непроблемной (вы можете помочь ей, определив ее в заголовке, используя lto и/или используя атрибуты компилятора, такие как __attribute__((always_inline)))

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

Вариант 2: две перегрузки

float calcLength(const float *v, int size, int inc) {
    float l = 0;

    for (int i=0; i<size*inc; i += inc) {
        l += v[i]*v[i];
    }
    return sqrt(l);
}

template <int Inc>
float calcLength(const float *v, int size) {
    float l = 0;

    for (int i=0; i<size*inc; i += inc) {
        l += v[i]*v[i];
    }
    return sqrt(l);
}

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

calcLength(v, size, inc); // ok
calcLength<1>(v, size);   // ok
calcLength(v, size, 1);   // nope

Вариант 3: Ваша версия

Ваша версия ок.

@geza отличное предложение, но, похоже, оно все еще требует значительной работы.

bolov 21.09.2019 11:07

Другие вопросы по теме