Inkrementální operátor a jeho rychlost

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.


Posted

in

,

by