Предположим, что у меня есть функция вычисления длины вектора, которая имеет дополнительный параметр 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 не является константой времени компиляции, вызывается общая функция@NathanOliver: насколько я понимаю, это то, что я делаю в вопросе. С одним отличием: чтобы разрешить оба вида inc, у него есть параметр шаблона типа. Но результат тот же.
Вы ищете если constexpr ?
Я имею в виду такую функцию, как template <std::size_t inc> float calcLength(const float *v, int size) { use inc here as a compile time value }.
@NathanOliver: ваше предложение имеет тот же результат, что и мое решение, если Constant<X> используется как inc.
Есть ли у вас какие-либо тесты, подтверждающие вашу конъюнктуру о «лучшей оптимизации»? Я могу изобразить гипотетические случаи, если inc % 8 == 0 или inc % 16 == 0, но не уверен, что векторизация будет намного лучше.
@Dmitry: в моем случае, если inc известно во время компиляции, это будет 1 99% времени. И, конечно же, его можно оптимизировать намного лучше. У него огромная разница в скорости.
@geza Я думаю, что «у вас есть тесты» означает «опубликуйте свой тест, чтобы я мог убедиться, что мое решение работает».
@anatolyg: у меня есть тесты, но не для этого простого случая. Но на самом деле этот вопрос не нуждается в эталоне. Это больше языковой вопрос. По сути, я хотел бы иметь функцию, которая может быть автоматически скомпилирована для константы времени компиляции. Мой код работает, но мне не нравится ручная спецификация Constant/Var. Я бы хотел, чтобы это было автоматически.
Итак, у вас есть доказательства того, что этот подход на самом деле лучше для вашей архитектуры, чем просто сделать функцию встроенной и позволить оптимизатору беспокоиться об этом? Это было бы хорошо объяснить или хотя бы упомянуть в вопросе.
@aschepler: я упомянул в вопросе, что у меня есть несколько функций, и они огромны. Я абсолютно уверен, что компилятор не будет их встраивать из-за их размера. Мне нужно использовать какую-то функцию forceinline. Но я не хочу, потому что размер скомпилированного кода будет намного больше. Было бы ошибкой встраивать эти функции в любую текущую архитектуру.
@JesperJuhl: я не знаю, как я могу использовать if constexpr в своей проблеме, так что, вероятно, нет.
Но если inc не является константой времени компиляции, а имеет значение 1, какую функцию следует вызывать? Для вас нормально, если она называется Constant<1> версией?
@ max66: в основном это никогда не бывает. Но я думаю, что понимаю, почему вы об этом спрашиваете: я мог бы добавить небольшую встроенную функцию-оболочку, которая проверяет 1. Мне не очень нравится это решение, потому что оно добавляет ненужное if для случая константы времени не компиляции. Если это единственное решение, я буду продолжать использовать Constant/Var.
Теперь, когда я включил оптимизацию, обе версии вычислялись во время компиляции: godbolt.org/z/-s1xNS
Если вы не слишком возражаете против переносимости, __builtin_constant_p — отличный инструмент для такого рода оптимизации.
Если вы используете GCC, как предложил @MarcGlisse, __builtin_constant_p будет работать, но если в этом примере все еще условия времени выполнения...





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);
});
}
Хороший улов на отсутствующем возврате, исправлено
Добавлена ссылка на Godbolt
Могут ли компиляторы делать то, что вы хотите, не пошевелив пальцем (ну, вам нужно включить оптимизированные сборки, но это само собой).
Компиляторы могут создавать так называемые «клоны функций», которые делают то, что вы хотите. Функция клонирования — это копия функции, используемой для распространения констант, то есть результирующая сборка функции, вызываемой с постоянными аргументами. Я нашел мало документации об этой функции, поэтому вам решать, хотите ли вы полагаться на нее.
Компилятор может полностью встроить эту функцию, потенциально делая вашу проблему непроблемной (вы можете помочь ей, определив ее в заголовке, используя lto и/или используя атрибуты компилятора, такие как __attribute__((always_inline)))
Теперь я не проповедую, чтобы компилятор делал свою работу. Несмотря на то, что в наше время оптимизация компилятора потрясающая, и эмпирическое правило состоит в том, чтобы доверять оптимизатору, бывают ситуации, когда вам нужно вмешиваться вручную. Я просто говорю, чтобы знать и принять это во внимание. О, и, как всегда мера, мера, мера, когда дело доходит до производительности, не используйте интуицию «Я чувствую, что мне нужно оптимизировать».
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
Ваша версия ок.
@geza отличное предложение, но, похоже, оно все еще требует значительной работы.
Насколько я знаю, если вся функция
constexprне передает константу времени компиляции, это ничего вам не даст. Одна вещь, которую вы могли бы сделать, это сделать константу параметром шаблона, отличным от типа. Тогда значение будет известно во время компиляции, и компилятор сможет соответствующим образом оптимизировать.