2016-11-16 6 views
6

У меня есть функция, которая находит следующую мощность двух для заданного целого. Если целое число равно двум, оно возвращает мощность.Почему компилятор C++ не может оптимизировать «if (test) --foo» to «foo - = test»?

Довольно прямо вперед:

char nextpow2if(int a) 
{ 
    char foo = char(32 - __builtin_clz(a)); 
    bool ispow2 = !(a & a-1); 
    if (ispow2) --foo; 
    return foo; 
} 

Однако после компиляции с GCC 6 с -O2, после осмотра сгенерированного сборки, я вижу, что это компилируется, казалось бы, бесполезные инструкции cmovne после вычисления Foo-1. Еще хуже с gcc5 и старше я получаю фактический jne ветвь в коде.

Чем быстрее способ собрать это было бы, как если бы я написал следующую функцию:

char nextpow2sub(int a) 
{ 
    char foo = char(32 - __builtin_clz(a)); 
    bool ispow2 = !(a & a-1); 
    return foo - ispow2; 
} 

Этот код правильно составленная все компиляторами кратчайший (и быстрым), монтаж возможен с sete и вычитанием для bool.

Почему компилятор не может оптимизировать первый? Это похоже на очень простой случай идентификации. Почему gcc 5 и старше компилируют это в фактическую ветку jne? Есть ли граничный случай между двумя версиями, который я не вижу, что может привести к тому, что они будут вести себя по-другому?

PS: Живая демонстрация here

Edit: Я не тестировал производительность с помощью GCC 6, но с ССЗ 5 последних примерно в два раза быстрее (ну на синтетическом тесте performanse, по крайней мере). Именно это и побудило меня задать этот вопрос.

+1

Не спам-теги для несвязанных языков! – Olaf

+0

* «Более быстрый способ скомпилировать это будет так, как если бы я написал следующую функцию:« * Вы измерили это? Насколько это быстрее? –

+0

Вы сравниваете производительность по количеству сгенерированных ассемблерных кодов? Это не лучший способ сделать это (хотя в некоторых случаях это может быть правдой). – Arunmu

ответ

0

Я считаю, что причиной этого может быть то, что bool обычно хранится в байте. Следовательно, компилятор может быть не в состоянии безопасно предположить, что фактическая память равна точно равна 1. Проверка true/false, вероятно, просто сравнивается с нулем. Вычитание, однако, может быть другой историей с побочными эффектами.

См example code on Ideone:

#include <iostream> 
using namespace std; 

union charBool 
{ 
    unsigned char aChar; 
    bool aBool; 
}; 

int main() 
{ 
    charBool var; 
    charBool* varMemory = &var; 

    var.aBool = 65; 
    std::cout << "a boolean = " << var.aBool << std::endl; 
    std::cout << "a char = " << var.aChar << std::endl; 
    std::cout << "varMemory = " << (*(reinterpret_cast<unsigned char*>(varMemory))) << std::endl; 

    var.aChar = 98; // note: Ideone C++ compiler resolves this to zero, hence bit0 seems to be the only checked 
    std::cout << "a boolean = " << var.aBool << std::endl; 
    std::cout << "a char = " << var.aChar << std::endl; 
    std::cout << "varMemory = " << (*(reinterpret_cast<unsigned char*>(varMemory))) << std::endl; 

    return 0; 
} 

Это приводит к:

a boolean = 1 
a char = 
varMemory = 
a boolean = 0 
a char = b 
varMemory = b 

(примечание: первые два символа является нецензурным)

+1

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

+0

Также OT: Тип punning через 'union' - UB. –

+0

@BaummitAugen кратко: кажется дешевым предположить, что любое значение, отличное от нуля, в булевом языке - это 'истина'. Если вы хотите включить эту оптимизацию «точно 1 для уменьшения», чтобы выполнить первое - вам придется всегда проверять наличие побочных эффектов (т. Е. Если кто-то основывается на = 1). Код выше - это игра для изменения булевой внутренней памяти (поскольку назначение может установить ее в 0/1). – hauron

0

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

char nextpow2sub(int a) 
{ 
    char foo = char(32 - __builtin_clz(a)); 
    bool ispow2 = !(a & a-1); 
    return foo - (5 * ispow2); 
} 

char nextpow2if(int a) 
{ 
    char foo = char(32 - __builtin_clz(a)); 
    bool ispow2 = !(a & a-1); 
    if (ispow2) foo = foo - 5; 
    return foo; 
} 

Единственное изменение, которое я сделал здесь то, что я вычитанием на 5, а не 1. При компиляции с помощью GCC 6.x и сравнить, вы увидите, что генерируемый двоичный код имеет одинаковый размер для обеих функций. Я ожидаю, что и у них будет более или менее одинаковая производительность.

Это показывает, что алгоритм оптимизации, используемый компилятором, был разработан для обработки общего случая. Тем не менее, даже для случая вычитания на 1, я ожидаю (используя gcc 6.x), что будет небольшая разница в производительности на любом современном процессоре, который поддерживает параллелизм на уровне инструкций и переименование регистра.

Этого код правильно составлен все составители до самого короткого (и) быстрого монтажа возможен с sete и вычитания для bool.

Как вы узнали, что это самый короткий и быстрый код? Да, это короче и быстрее, но есть ли у вас доказательства того, что это самый короткий и быстрый? Также вы не можете дать такое заявление без указания конкретной архитектуры и микроархитектуры.

 Смежные вопросы

  • Нет связанных вопросов^_^