производителност на оценяване на g++ c++11 constexpr

g++ (4.7.2) и подобни версии изглежда оценяват constexpr изненадващо бързо по време на компилиране. На моите машини всъщност много по-бързо от компилираната програма по време на изпълнение.

Има ли разумно обяснение за това поведение? Има ли включени техники за оптимизация, които са приложими само по време на компилиране, които могат да бъдат изпълнени по-бързо от действително компилирания код? Ако да, кое?

Ето моята тестова програма и наблюдаваните резултати.

#include <iostream>

constexpr int mc91(int n)
 {

     return (n > 100)? n-10 : mc91(mc91(n+11));

 }

constexpr double foo(double n)
{
   return (n>2)? (0.9999)*((unsigned int)(foo(n-1)+foo(n-2))%100):1;
}

constexpr unsigned ack( unsigned m, unsigned n )
{
    return m == 0
        ? n + 1
        : n == 0
        ? ack( m - 1, 1 )
        : ack( m - 1, ack( m, n - 1 ) );
}

constexpr unsigned slow91(int n) {
   return mc91(mc91(foo(n))%100);
}

int main(void)
{
   constexpr unsigned int compiletime_ack=ack(3,14);
   constexpr int compiletime_91=slow91(49);
   static_assert( compiletime_ack == 131069, "Must be evaluated at compile-time" );
   static_assert( compiletime_91  == 91,     "Must be evaluated at compile-time" );
   std::cout << compiletime_ack << std::endl;
   std::cout << compiletime_91  << std::endl;
   std::cout << ack(3,14) << std::endl;
   std::cout << slow91(49) << std::endl;
   return 0;
}

време на компилиране:

time g++ constexpr.cpp -std=c++11 -fconstexpr-depth=10000000 -O3 

real    0m0.645s
user    0m0.600s
sys     0m0.032s

време на изпълнение:

time ./a.out 

131069
91
131069
91

real    0m43.708s
user    0m43.567s
sys     0m0.008s

Тук mc91 е обичайният mac carthy f91 (както може да се намери в wikipedia), а foo е просто безполезна функция, връщаща реални стойности между около 1 и 100, с fib сложност по време на изпълнение.

Както бавното изчисление на 91, така и функциите на ackermann се оценяват с едни и същи аргументи от компилатора и компилираната програма.

Изненадващо, програмата дори би работила по-бързо, просто генерирайки код и го пускайки през компилатора, отколкото изпълнявайки самия код.


person i_want_to_learn    schedule 25.04.2013    source източник
comment
Откъде знаете, че компилаторът изчислява някой от тези изрази?   -  person n. 1.8e9-where's-my-share m.    schedule 25.04.2013
comment
Същият проблем по време на този сравнителен тест. Прочетете последния параграф.   -  person masoud    schedule 25.04.2013
comment
Редактиране: Добавен static_assert, както е предложено от Дрю Дорман, за да направи примера по-убедителен (не променя резултатите.)   -  person i_want_to_learn    schedule 25.04.2013


Отговори (2)


По време на компилиране, излишните (идентични) constexpr извиквания могат да бъдат мемоизирани, докато рекурсивното поведение по време на изпълнение не осигурява това.

Ако промените всяка рекурсивна функция като...

constexpr unsigned slow91(int n) {
   return mc91(mc91(foo(n))%100);
}

... към формуляр, който не е constexpr, но запомня минали изчисления по време на изпълнение:

std::unordered_map< int, boost::optional<unsigned> > results4;
//     parameter(s) ^^^           result ^^^^^^^^

unsigned slow91(int n) {
     boost::optional<unsigned> &ret = results4[n];
     if ( !ret )
     {
         ret = mc91(mc91(foo(n))%100);
     }
     return *ret;
}

Ще получите по-малко изненадващи резултати.

време на компилиране:

time g++ test.cpp -std=c++11 -O3

real    0m1.708s
user    0m1.496s
sys     0m0.176s

време на изпълнение:

time ./a.out

131069
91
131069
91

real    0m0.097s
user    0m0.064s
sys     0m0.032s
person Drew Dormann    schedule 25.04.2013
comment
Присвояването на променлива constexpr е един от случаите, когато оценката по време на изпълнение не е разрешена. - person bames53; 25.04.2013
comment
@bames53 Да, чудя се дали gcc заобикаля стандарта в този случай. Подозирам, че тестването на двата реда в този отговор ще покаже. - person Drew Dormann; 25.04.2013
comment
вижте отговора ми, стойностите наистина се оценяват по време на компилация (както е доказано изхода ми на асемблер).. и със сигурност static_assert ще докаже същото нещо. - person Filip Roséen - refp; 25.04.2013
comment
Да, оценяването на израз с помощта на едни и същи аргументи веднъж (мемоизация) беше моята теория на първо място, следователно реално номерираната функция на фибоначи като foo, за която тази техника не е разумно приложима. Относно дълбочината на рекурсия: това е така, ние сме извън стандарта тук, но използването на такива големи стойности по-добре демонстрира наблюдавания ефект. - person i_want_to_learn; 25.04.2013
comment
Освен това, тъй като прости техники като мемоизация могат да бъдат изключени за функции с реална стойност, които все още се оценяват по-бързо по време на компилиране, този клас проблеми може да постави точка в полза на интерпретираните езици, за които са възможни трансформации на ниво източник по време на изпълнение? - person i_want_to_learn; 26.04.2013

Мемоизация

Това е много интересно "откритие", но отговорът вероятно е по-прост, отколкото си мислите.

Нещо може да бъде оценено по време на компилиране, когато е декларирано constexpr, ако всички включени стойности са известни по време на компилиране (и ако променливата, където стойността трябва да завърши, е декларирана като constexpr като добре) с това казано, представете си следния псевдокод:

f(x)   = g(x)
g(x)   = x + h(x,x)
h(x,y) = x + y

тъй като всяка стойност е известна по време на компилация, компилаторът може да пренапише горното в еквивалентното по-долу:

f(x) = x + x + x

Казано с думи, всяко извикване на функция е премахнато и заменено с това на самия израз. Това, което също е приложимо, е метод, наречен мемоизация, при който резултатите от предадени изчислени изрази се съхраняват далеч, така че вие ​​само трябва да свърша тежката работа веднъж.

Ако знаете, че g(5) = 15 защо го изчислявате отново? вместо това просто заменете g(5) с 15 всеки път, когато е необходимо. Това е възможно, тъй като на функция, декларирана като constexpr, не е разрешено да има странични ефекти.


Време за изпълнение

По време на изпълнение това не се случва (тъй като не сме казали на кода да се държи по този начин). Малкото момче, което минава през вашия код, ще трябва да скочи от f до g до h и след това да прескочи до g от h, преди да прескочи от g до f, докато съхранява върнатата стойност на всяка функция и я предава на следващата .

Дори и този човек да е много, много мъничък и да не му се налага да скача много, много, той все още не обича да скача напред-назад през цялото време, отнема му много, за да направи това и това; отнема време.


Но в примера с OP наистина ли се изчислява времето за компилиране?

Да, и на тези, които не вярват, че компилаторът действително изчислява това и го поставя като константи в готовия двоичен файл, ще предоставя съответните инструкции за асемблиране от OPs кода по-долу (изход от g++ -S -Wall -pedantic -fconstexpr-depth=1000000 -std=c++11)

main:
.LFB1200:
  .cfi_startproc
  pushq %rbp
  .cfi_def_cfa_offset 16
  .cfi_offset 6, -16
  movq  %rsp, %rbp
  .cfi_def_cfa_register 6
  subq  $16, %rsp
  movl  $131069, -4(%rbp)
  movl  $91, -8(%rbp)
  movl  $131069, %esi               # one of the values from constexpr
  movl  $_ZSt4cout, %edi
  call  _ZNSolsEj
  movl  $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
  movq  %rax, %rdi
  call  _ZNSolsEPFRSoS_E
  movl  $91, %esi                   # the other value from our constexpr
  movl  $_ZSt4cout, %edi
  call  _ZNSolsEi
  movl  $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi
  movq  %rax, %rdi

  # ...
  # a lot of jumping is taking place down here
  # see the full output at http://codepad.org/Q8D7c41y
person Filip Roséen - refp    schedule 25.04.2013