2016-01-17 4 views
1

Рассмотрим следующий код:В функции удаления ветви, когда член известно во время компиляции

// Class definition 
class myclass 
{ 
    public: 
    constexpr myclass() noexcept: _value{0}, _option{true} {} 
    constexpr myclass(int value) noexcept: _value{value}, _option{true} {} 
    constexpr myclass(int value, bool option) noexcept: _value{value}, _option{option} {} 
    constexpr int get_value() const noexcept {return _value;} 
    constexpr int get_option() const noexcept {return _option;} 
    private: 
    int _value; 
    bool _option; 
}; 

// Some function that should be super-optimized 
int f(myclass x, myclass y) 
{ 
    if (x.get_option() && y.get_option()) { 
     return x.get_value() + y.get_value(); 
    } else { 
     return x.get_value() * y.get_value(); 
    } 
} 

Мой вопрос заключается в следующем: в такой модели, компиляторы обычно в состоянии избежать теста, когда опция известный во время компиляции, например, когда f(a, b) вызывается с целыми числами a и b (в этой ситуации вызывается неявный однопараметрический конструктор, а option всегда верен)? Когда я говорю «вообще», я имею в виду сложную программу реального мира, но где f вызывается на двух int.

ответ

1

Короткий ответ «зависит». Это зависит от множества вещей, включая сложность кода, используемый компилятор и т. Д.

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

Итак, предполагая, что компилятор может «следовать» «код (например, если f и вызов f находятся в разных исходных файлах, вряд ли он будет оптимизирован).

Конечно, если вы хотите быть уверенным, что ваш конкретный компилятор делает с ВАШИМ конкретным кодом, вам нужно будет проверить код, сгенерированный компилятором.

// Class definition 
class myclass 
{ 
    public: 
    constexpr myclass() noexcept: _value{0}, _option{true} {} 
    constexpr myclass(int value) noexcept: _value{value}, _option{true} {} 
    constexpr myclass(int value, bool option) noexcept: _value{value}, _option{option} {} 
    constexpr int get_value() const noexcept {return _value;} 
    constexpr int get_option() const noexcept {return _option;} 
    private: 
    int _value; 
    bool _option; 
}; 

// Some function that should be super-optimized 
int f(myclass x, myclass y) 
{ 
    if (x.get_option() && y.get_option()) { 
     return x.get_value() + y.get_value(); 
    } else { 
     return x.get_value() * y.get_value(); 
    } 
} 

int main() 
{ 
    myclass a; 
    myclass b(1); 
    myclass c(2, false); 

    int x = f(a, b); 
    int y = f(b, c); 

    return x + y; 
} 

Это будет генерировать код, который идентичен:

int main() 
{ 
    return 3; 
} 

Однако, если мы изменим код для этого:

#include "myclass.h" 

extern int f(myclass x, myclass y); 

int main() 
{ 
    myclass a; 
    myclass b(1); 
    myclass c(2, false); 

    int x = f(a, b); 
    int y = f(b, c); 

    return x + y; 
} 

и объявить f в отдельном файле (с -O2 оптимизация), полученный код

define i32 @_Z1f7myclassS_(i64 %x.coerce, i64 %y.coerce) #0 { 
entry: 
    %x.sroa.0.0.extract.trunc = trunc i64 %x.coerce to i32 
    %y.sroa.0.0.extract.trunc = trunc i64 %y.coerce to i32 
    %conv.i = and i64 %x.coerce, 1095216660480 
    %tobool = icmp eq i64 %conv.i, 0 
    %conv.i12 = and i64 %y.coerce, 1095216660480 
    %tobool2 = icmp eq i64 %conv.i12, 0 
    %or.cond = or i1 %tobool, %tobool2 
    %add = add nsw i32 %y.sroa.0.0.extract.trunc, %x.sroa.0.0.extract.trunc 
    %mul = mul nsw i32 %y.sroa.0.0.extract.trunc, %x.sroa.0.0.extract.trunc 
    %retval.0 = select i1 %or.cond, i32 %mul, i32 %add 
    ret i32 %retval.0 
} 

и главный:

define i32 @main() #0 { 
entry: 
    %call = tail call i32 @_Z1f7myclassS_(i64 4294967296, i64 4294967297) 
    %call4 = tail call i32 @_Z1f7myclassS_(i64 4294967297, i64 2) 
    %add = add nsw i32 %call4, %call 
    ret i32 %add 
} 

Как можно видеть, аргумент f преобразуется в двух 64-битных чисел, а значение option хранится в верхней половине 64-битового значения. Затем функция f разбивает 64-битное значение на две части и решает, возвращать ли результат умножения или сложения на основе значения.

+0

Что вы подразумеваете под «различными исходными файлами». Что делать, если 'f' определено в файле заголовка? – Vincent

+0

Если 'f' находится в файле заголовка, он может быть встроен там, где он вызывается. Если 'f' объявлен, но не определен, компилятор не будет иметь никакой опции, кроме как вызвать функцию - и поскольку вызываемая функция не знает, что передается, необходимо проверить входные значения. Я вернусь немного с несколькими примерами, показывая, что я имею в виду. –

+1

LTO позволяет даже в том случае, если он определен в другой единицы перевода! – kukyakya