Code snippets, ideas and events from IT related projects

by Robert Gawron

3 sposoby na zamianę wartości zmiennych: analiza szybkości

Intro

Często zachodzi potrzeba zamienienia między sobą wartości zmiennych, co zazwyczaj wykonuje się przy użyciu dodatkowej zmiennej, jednak są inne możliwości, np użycie XOR'a. Sprawdzimy, która z tych metod jest bardziej wydajna, oraz to, jak kompilator optymalizuje nasz kod w C. BTW to pierwszy post tutaj z asemblerem w tle =)

Rzecz dzieje się na poziomie procesora i jego instrukcji (mnemoników), stąd wykorzystamy kod w C i asemblerze. Przypomnijmy, że gdy kompilujemy przy użyciu gcc program podając flagę -S otrzymamy plik z kodem asemblera, który możemy przeanalizować/zmodyfikować i skompilować do kodu wynikowego. Flaga -pg pozwala analizować gprof'em czas wykonywania funkcji w programie. Więcej info o tym jest w tym poście (wiem.. ciągle go nie skończył).

W gcc poziom optymalizacji zmienia flaga -O, (O1 < O2 < O3). Spróbujmy dla poniższego kodu, dla kolejnych poziomów optymalizacji zobaczyć, jak wygląda kod asemblera wynikowy (z którego został skompilowany kod w C i który zostanie skompilowany do kodu wynikowego). Wydajność obu metod zbadamy gprof'em.

   1  #include <stdio.h>
   2  
   3  void swapByXOR(int *a, int *b){
   4      // we dont care if a is b
   5      *a = *a^*b;
   6      *b = *a^*b;
   7      *a = *a^*b;
   8  }
   9  
  10  int swapByBuffer(int *a, int *b){
  11      int c;
  12      c = *a;
  13      *a = *b;
  14      *b = c;
  15  }
  16  
  17  int main(){
  18      int a = 0x44, b = 0x67;
  19      int i, max = 400000000;
  20  
  21      for(i=0; i<max; i++)
  22          swapByBuffer(&a, &b);
  23      for(i=0; i<max; i++)
  24          swapByXOR(&a, &b);
  25  }
  26  

Bez optymalizacji:

rgawron@rgawron-laptop:~/testsub$ gcc -O0 -S merge.c
rgawron@rgawron-laptop:~/testsub$ cat merge.s
	.file	"merge.c"
	.text
.globl swapByXOR
	.type	swapByXOR, @function
swapByXOR:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %eax
	movl	(%eax), %edx
	movl	12(%ebp), %eax
	movl	(%eax), %eax
	xorl	%eax, %edx
	movl	8(%ebp), %eax
	movl	%edx, (%eax)
	movl	8(%ebp), %eax
	movl	(%eax), %edx
	movl	12(%ebp), %eax
	movl	(%eax), %eax
	xorl	%eax, %edx
	movl	12(%ebp), %eax
	movl	%edx, (%eax)
	movl	8(%ebp), %eax
	movl	(%eax), %edx
	movl	12(%ebp), %eax
	movl	(%eax), %eax
	xorl	%eax, %edx
	movl	8(%ebp), %eax
	movl	%edx, (%eax)
	popl	%ebp
	ret
	.size	swapByXOR, .-swapByXOR
.globl swapByBuffer
	.type	swapByBuffer, @function
swapByBuffer:
	pushl	%ebp
	movl	%esp, %ebp
	subl	$16, %esp
	movl	8(%ebp), %eax
	movl	(%eax), %eax
	movl	%eax, -4(%ebp)
	movl	12(%ebp), %eax
	movl	(%eax), %edx
	movl	8(%ebp), %eax
	movl	%edx, (%eax)
	movl	12(%ebp), %edx
	movl	-4(%ebp), %eax
	movl	%eax, (%edx)
	leave
	ret
	.size	swapByBuffer, .-swapByBuffer
.globl main
	.type	main, @function
main:
	leal	4(%esp), %ecx
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%ecx
	subl	$24, %esp
	movl	$68, -8(%ebp)
	movl	$103, -12(%ebp)
	movl	$400000000, -20(%ebp)
	movl	$0, -16(%ebp)
	jmp	.L6
.L7:
	leal	-12(%ebp), %eax
	movl	%eax, 4(%esp)
	leal	-8(%ebp), %eax
	movl	%eax, (%esp)
	call	swapByBuffer
	addl	$1, -16(%ebp)
.L6:
	movl	-16(%ebp), %eax
	cmpl	-20(%ebp), %eax
	jl	.L7
	movl	$0, -16(%ebp)
	jmp	.L9
.L10:
	leal	-12(%ebp), %eax
	movl	%eax, 4(%esp)
	leal	-8(%ebp), %eax
	movl	%eax, (%esp)
	call	swapByXOR
	addl	$1, -16(%ebp)
.L9:
	movl	-16(%ebp), %eax
	cmpl	-20(%ebp), %eax
	jl	.L10
	addl	$24, %esp
	popl	%ecx
	popl	%ebp
	leal	-4(%ecx), %esp
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 4.2.3 (Ubuntu 4.2.3-2ubuntu7)"
	.section	.note.GNU-stack,"",@progbits

Każdy mnemonik (pojedyńcza instrukcja) zużywa stalą (dla danego procka) ilość cykli, stąd im mniej instrukcji tym wydajniej działa dany fragment kodu. Również stosowanie instrukcji które zużywają mniej taktów zamiast tych bardziej kosztownych przyspiesza kod. Można by sie pokusić o przeliczenie ile taktów zużywa każda funkcja (z tego co pamiętam i xor i mov po jednym).

rgawron@rgawron-laptop:~/testsub$ gcc -O0 -pg merge.c
rgawron@rgawron-laptop:~/testsub$ ./a.out 
rgawron@rgawron-laptop:~/testsub$ gprof -b ./a.out 
Flat profile:

Each sample counts as 0.01 seconds.
  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ns/call  ns/call  name    
 43.00      5.50     5.50 400000000    13.75    13.75  swapByXOR
 32.21      9.62     4.12                             main
 24.78     12.79     3.17 400000000     7.92     7.92  swapByBuffer

Optymalizacja 02:

	.file	"merge.c"
	.text
	.p2align 4,,15
.globl swapByXOR
	.type	swapByXOR, @function
swapByXOR:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %ecx
	movl	12(%ebp), %edx
	movl	(%ecx), %eax
	xorl	(%edx), %eax
	movl	%eax, (%ecx)
	xorl	(%edx), %eax
	movl	%eax, (%edx)
	xorl	%eax, (%ecx)
	popl	%ebp
	ret
	.size	swapByXOR, .-swapByXOR
	.p2align 4,,15
.globl swapByBuffer
	.type	swapByBuffer, @function
swapByBuffer:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %edx
	movl	12(%ebp), %ecx
	pushl	%ebx
	movl	(%edx), %ebx
	movl	(%ecx), %eax
	movl	%eax, (%edx)
	movl	%ebx, (%ecx)
	popl	%ebx
	popl	%ebp
	ret
	.size	swapByBuffer, .-swapByBuffer
	.p2align 4,,15
.globl main
	.type	main, @function
main:
	leal	4(%esp), %ecx
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%edi
	pushl	%esi
	pushl	%ebx
	movl	$1, %ebx
	pushl	%ecx
	subl	$24, %esp
	leal	-24(%ebp), %edi
	leal	-20(%ebp), %esi
	movl	$68, -20(%ebp)
	movl	$103, -24(%ebp)
	movl	%edi, 4(%esp)
	movl	%esi, (%esp)
	call	swapByBuffer
	.p2align 4,,7
.L6:
	movl	%edi, 4(%esp)
	addl	$1, %ebx
	movl	%esi, (%esp)
	call	swapByBuffer
	cmpl	$400000000, %ebx
	jne	.L6
	movl	%edi, 4(%esp)
	movl	$1, %ebx
	movl	%esi, (%esp)
	call	swapByXOR
	.p2align 4,,7
.L8:
	movl	%edi, 4(%esp)
	addl	$1, %ebx
	movl	%esi, (%esp)
	call	swapByXOR
	cmpl	$400000000, %ebx
	jne	.L8
	addl	$24, %esp
	popl	%ecx
	popl	%ebx
	popl	%esi
	popl	%edi
	popl	%ebp
	leal	-4(%ecx), %esp
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 4.2.3 (Ubuntu 4.2.3-2ubuntu7)"
	.section	.note.GNU-stack,"",@progbits

Kompilator nie tworzy dodatkowej zmiennej, zamiast tego odkłada obie na stos a nastepnie pobiera je w odwrotnej koljeności.

  %   cumulative   self              self     total           
 time   seconds   seconds    calls  ns/call  ns/call  name    
 43.65      2.44     2.44 400000000     6.10     6.10  swapByXOR
 30.77      4.16     1.72 400000000     4.30     4.30  swapByBuffer
 25.58      5.59     1.43                             main

Optymalizacja O3:

	.file	"merge.c"
	.text
	.p2align 4,,15
.globl swapByXOR
	.type	swapByXOR, @function
swapByXOR:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %ecx
	movl	12(%ebp), %edx
	movl	(%ecx), %eax
	xorl	(%edx), %eax
	movl	%eax, (%ecx)
	xorl	(%edx), %eax
	movl	%eax, (%edx)
	xorl	%eax, (%ecx)
	popl	%ebp
	ret
	.size	swapByXOR, .-swapByXOR
	.p2align 4,,15
.globl swapByBuffer
	.type	swapByBuffer, @function
swapByBuffer:
	pushl	%ebp
	movl	%esp, %ebp
	movl	8(%ebp), %edx
	movl	12(%ebp), %ecx
	pushl	%ebx
	movl	(%edx), %ebx
	movl	(%ecx), %eax
	movl	%eax, (%edx)
	movl	%ebx, (%ecx)
	popl	%ebx
	popl	%ebp
	ret
	.size	swapByBuffer, .-swapByBuffer
	.p2align 4,,15
.globl main
	.type	main, @function
main:
	leal	4(%esp), %ecx
	andl	$-16, %esp
	pushl	-4(%ecx)
	pushl	%ebp
	movl	%esp, %ebp
	pushl	%ecx
	popl	%ecx
	popl	%ebp
	leal	-4(%ecx), %esp
	ret
	.size	main, .-main
	.ident	"GCC: (GNU) 4.2.3 (Ubuntu 4.2.3-2ubuntu7)"
	.section	.note.GNU-stack,"",@progbits

Tu jest najmilej :) Kompilator zauważył, iż kod zmieniający wartość zmiennych i nigdzie tego nie wykorzystujący jest zbędny i usunął jego wywołanie w main'ie. Nie ma co odpalać gprof'a, bo nasze funkcje nie zostaną wywołane.

Wnioski?

Metoda z XOR'em jest tylko ciekawostką. Angielska Wikipedia podaje, iż przydaje się ona w sytuacjach gdy ilość miejsca jest ograniczona, np. w systemach wbudowanych.

Linki, sznurki i pętelki

Pingbacks

No pingbacks yet

Comments

avatar
ajd , 8.08.2008 22:23, reply
Hmm, z Toba moge porozmawiac, z tymi oszolomami niezbyt mam ochote. Tak wiec dlaczego uwazam, ze jak to ujalem "duzo latwiej i bardziej elegancko pisze sie program, ktory wykorzystuje slabosci systemu Uniksowego anizeli Windowsa." 1) Otwarty kod -> implikuje wieksza wykrywalnosc podatnosci, ktore moga zostac wyexploitowane bardziej precyzyjnie. 2) Opisane f-cje systemowe (jezeli potrzebny, mozliwy wglad do zrodla co zwieksza efektywnosc narzedzia) 3) Loadable Kernel Modules (1) -> 0-day kernel exploit -> Injecting Evil LKM i po zabawie) I to by byly chyba glowne powodu dla ktorych IMHO latwiej jest skodzic jakas paskude na Linuksa anizeli na Windowsa. OK, troche wyolbrzymilem z tym *duzo*, *duzo* moze i nie ale jednak latwiej. PS. "I don't really love you." lcamtuf
avatar
Robert , 9.08.2008 9:16, reply
ad1, ad2: ..ale też dzięki temu, że kod jest otwarty ktoś może zgłosić lukę czy zaproponować swoją poprawkę zanim zostanie ona wykorzystana do niewłaściwych celów przez innych więc to działa też w drugą stronę. Więc jak przeglądasz otwarty kod to wielu luk które mógłbyś użyć już nie ma bo usunęli je inni. ad3: Czy podobny problemu nie ma Windows: http://forum.cc-team.org/viewtopic.php?t=6850 Ja się w tym przypadku nie znam więc się nie wypowiem. ode mnie: Co do bezpieczeństwa zamkniętego kodu problemem jest też to, że nie wiadomo co się w nim dzieje i czy jest poprawny - musisz wierzyć na słowo. PS. "Smile cause Satan loves You"
avatar
ajd , 9.08.2008 11:30, reply
ad1, ad2: Taki model sprawdza sie w mniejszych aplikacjach. Jezeli wezmiemy pod uwage cos wiekszego albo bardziej krytycznego (ie. kernel) to drastycznie spada ilosc ludzi, ktorzy znajduja w niej bugi przy czym jestem pewien, ze jest wiecej tych *zlych*. Btw: http://www.pcpro.co.uk/news/213213/torvalds-rages-at-linux-security-zealots.html ad3: Nie twierdze, ze Windows jest bezpieczny, wiem za to, ze latwiej jest napisac -- przynajmniej dla mnie -- *cos zlego* dla Linuksa anizeli dla Windowsa (chociazby dlatego, ze Linuks wykazuje sie wieksza logika co w rezultacie prowadzi do mniejszego kodu - mniej kodu wieksze szanse ukrycia). Poza tym trudniej znalezc krytyczna luke w tak duzym, zamknietym systemie jak Windows anizeli w otwartym, mniejszym systemie jakim jest Linuks. PS. Nie wiem czy dobrze mnie zrozumiales - "I don't really love you" to tekst Zalewskiego, wpisz w google.
avatar
ajd , 9.08.2008 11:31, reply
Maj mistejk: "I dont think I really love you." -> http://209.85.135.104/search?q=cache:Gr4oUXb8Dt0J:lcamtuf.coredump.cx/worm.txt+lcamtuf+worm.txt&hl=pl&ct=clnk&cd=1&gl=pl
avatar
ajd , 9.08.2008 11:33, reply
Maj mistejk -> "I don't think I really love you". http://tinyurl.com/5jrapg
avatar
Robert , 9.08.2008 12:47, reply
ad1, ad2, ciekawy link, muszę się nad tym zastanowić. Generalnie nie wiem, co odpowiedzieć :) Wiem, kim jest Lcamtuf :->, dokumentu w którym to powiedział nie kojarzyłem - thx za link, przeczytam.

Leave your reply

Let me know what you think

Required. 30 chars of fewer.

Required.

captcha image