Dnešní článek je spíše zamyšlením, nad tím, co jako programátoři víme a máme ověřené vůči tomu, co si myslíme. Kdysi dávno celou řadu z nás učili, že pokud použijeme inkrementální operátor v jeho prefix formě, bude výkon aplikace vyšší.
Prefix forma je uvedena na příkladu níže (++i)
#include <iostream> int main(int argc, char *argv[]) { for(int i = 0; i < 10; ++i) { std::cout << "Loop " << i << std::endl; } }
To, že bude výkon vyšší většina ‚školitelů‘ vykládá tím, že v přímém přepisu do jazyka assembler, je hodnota, která má být vrácena je nejdříve zvýšena o jednotku a až poté přímo navrácena. Zatímco v případě opačném, tedy suffix formě je nutné vytvořit lokální pomocnou proměnnou, do které se uloží předchozí hodnota, která se má navrátit. Teprve poté je možné tuto hodnotu (proměnnou) zvětšit o jednu jednotku.
Pokud i vám toto někdy někdo říkal, samozřejmě měl pravdu, nicméně ve chvíli, kdy se nepíše program přímo v jazyce assembler, ale například v jazyce C, nebo C++, vstupuje do pomyslné hry ještě kompilátor. Vzhledem k tomu, že kompilátor je takové dost divné zvíře, které má právo naprosto zamíchat kartami, podíváme se mu trochu na zoubek.
Naivní představa, jak toto otestovat spočívá v tom, že si programátor napíše dva obdobné programy a v jednom z nich použije suffix formu, ve druhém prefix formu a následně porovná dobu běhu. Tato představa je naivní především proto, že do výsledného času běhu se promítne i zbytek systému, který může využívat procesor velmi nestejnoměrně.
Lepší způsob a současně i zde prezentovaný postup spočívá v tom, že programátor rovněž vytvoří dva obdobné programy. Nicméně požádá kompilátor o to, aby zobrazil výsledný kód v jazyce assembler. Více o jazyce assemebler na wiki.
Následující kompilační příkaz požádá kompilátor o to, aby zobrazil výsledný kód v jazyce assembler.
g++ -Os -S main-pre.cpp g++ -Os -S main-post.cpp diff main-pre.s main-post.s
Parametry předané kompilátoru:
- -Os kompilátor provede optimalizaci s důrazem na výslednou velikost
- -S výstupem kompilace bude soubor se stejným jménem, a bude obsahovat assembler kód
Výsledný zdrojový kód v jazyce assembler
.file "main-pre.cpp" .text .section .text.startup,"ax",@progbits .globl main .type main, @function main: .LFB1519: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 leaq _ZSt4cout(%rip), %rbp pushq %rbx .cfi_def_cfa_offset 24 .cfi_offset 3, -24 xorl %ebx, %ebx pushq %rcx .cfi_def_cfa_offset 32 .L2: movl %ebx, %esi movq %rbp, %rdi incl %ebx call _ZNSolsEi@PLT cmpl $100, %ebx jne .L2 popq %rdx .cfi_def_cfa_offset 24 xorl %eax, %eax popq %rbx .cfi_def_cfa_offset 16 popq %rbp .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE1519: .size main, .-main .type _GLOBAL__sub_I_main, @function _GLOBAL__sub_I_main: .LFB2000: .cfi_startproc pushq %rax .cfi_def_cfa_offset 16 leaq _ZStL8__ioinit(%rip), %rdi call _ZNSt8ios_base4InitC1Ev@PLT movq _ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rdi popq %rcx .cfi_def_cfa_offset 8 leaq __dso_handle(%rip), %rdx leaq _ZStL8__ioinit(%rip), %rsi jmp __cxa_atexit@PLT .cfi_endproc .LFE2000: .size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main .section .init_array,"aw" .align 8 .quad _GLOBAL__sub_I_main .local _ZStL8__ioinit .comm _ZStL8__ioinit,1,1 .hidden __dso_handle .ident "GCC: (Debian 8.2.0-20) 8.2.0" .section .note.GNU-stack,"",@progbits
Protože jsou oba výsledné zdrojové kódy v assembleru shodné, bude i vykonávaný program shodný.
Závěrem lze tedy říci, že kompilátor s příslušnými flagy (výsledek je stejný i bez -Os flagu) na architektuře x86 provede tuto optimalizaci za programátora. Je ovšem nutné upozornit čtenáře na to, že tyto výsledky platí pouze pro tento relativně jednoduchý příklad. Jak si kompilátor poradí v případě, že inkrementovaným objektem nebude číslo typu int, ale složitější datová struktura je otázkou pro další test.