컴파일러 비교하기

안상준·2025년 3월 6일

Virtualize Deobfuscator

목록 보기
1/14

GCC,Clang 과 옵션별 결과 비교하기

환경은 Ubuntu 에서 진행하였다.

옵션 종류

최적화 옵션 : -O0, -O1, -O2, -O3, -Ofast, -Os, -Oz

  • -O0 : 최적화가 없고 디버깅의 유리
  • -O1 : 기본적인 최적화
  • -O2 : 가장 많이 사용하는 최적화로 -O1에 추가적인 최적화 적용
  • -O3 : -O2보다 적극적인 최적화 수행
  • -Ofast : -O3 + 엄격한 표준 준수를 무시하는 최적화
  • -Os : 실행 속도보다 실행파일 크기 최소화가 목적
  • -Oz(Clang) : -Os 보다 더 강력한 크기 최적화 수행

옵션속도 최적화크기 최적화디버깅 가능성특징
-O0느림크다디버깅 용이최적화 없음
-O1약간 빠름약간 작음디버깅 가능기본적인 최적화
-O2빠름보통디버깅 어려움일반적인 최적화
-O3매우 빠름보통디버깅 매우 어려움적극적인 최적화, 루프 벡터화
-Ofast최고 속도보통디버깅 거의 불가능수학적 정확성 일부 희생
-Os보통작음디버깅 어려움코드 크기 최적화 중점
-Oz보통매우 작음디버깅 어려움Clang 적용, 최대 크기 최적화

GCC와 Clang

-O2옵션으로 gcc, clang으로 컴파일 하여 비교해 보기

코드는 while문 안에 switch문이 들어있는 코드를 사용

코드 동작은 0부터 9까지 짝수와 홀수를 구하여 출력하는 코드이다.

GCC

gcc -O2 -S test.c -o gcc_O2.s

gcc를 이용하여 컴파일한 main문 결과이다.
맨 처음 ecx, edx, eax를 0으로 초기화 시켜준다.
이후 l4로 점프하여 test를 통해 최하위 비트가 1인지 확인하여 홀수, 짝수를 구분한다.
홀수라면 l9으로 점프하여 eax(i)값을 증가 시키고, ecx값을 1 증가시킨다.
홀수일때 ecx를 증가시키므로 이는 변수 odd임을 유추할 수 있다.
.L4에서 짝수인 경우에는 점프를 하지 않고 eax, edx를 증가 시키고 edx는 변수 even임을 알 수 있다.
i가 10이 되면 출력문으로 jmp하는 조건문은 l9, l4에 존재하는 것을 알 수 있다.

Clang

clang -O2 -S test.c -o clang_O2.s
같은 c코드를 clang을 이용하여 컴파일 해 보았다.

main만 캡처한 사진이다.
gcc와 비교했을때 확연히 길이가 줄어든 것을 볼 수 있다.
특이한 점은 결과값을 계산하여 레지스터에 저장하고 바로 print를 하였다는 점이다.

이를 통해 clang이 gcc에 비해 최적화가 더 잘 되는 것을 볼 수 있었다.

GCC의 옵션 비교

이번에는 GCC의 옵션을 달리 하여 결과값을 비교해 보도록 하자.

-O0

gcc -O0 -S test.c -o gcc_O0.s

-O2옵션을 사용했을 때 보다 훨신 길어진 것을 볼 수 있다.
먼저 i, odd, even 변수의 값들을 스택에 저장을 한다.

movl    -12(%rbp), %edx
movl    %edx, %eax
sarl    $31, %eax
shrl    $31, %eax
addl    %eax, %edx
andl    $1, %edx
subl    %eax, %edx
movl    %edx, %eax
testl   %eax, %eax
je      .L3

이 부분이 나머지를 구하는 부분인데 eax에 -12(%rbp)를 저장하고
최상위 비트(부호비트)를 제외하고 0으로 만들어 준다.
그 다음 edx에 eax의 부호비트를 저장하고 edx의 최하위 비트를 제외하고 0으로 만들어 준다.
다시 부호비트를 제거하고 test를 통해 나머지가 1인지 확인하여 점프한다.
정리하면 음수의 상황을 고려하여 shift 연산을 이용하여 나머지를 구하는 코드이다.
l3, l4 는 각각 odd, even을 증가시켜주는 부분이다.

-Ofast

이번에는 -Ofast옵션을 이용하여 컴파일 해 보았다.
gcc -Ofast -S test.c -o gcc_Ofast.s

결과는 clang으로 컴파일 했을때와 동일한 것을 볼 수 있다. 레지스터에 출력값 5씩 저장하고 print 함수를 호출한다.

-Oz

이번에는 -Oz옵션을 사용하여 분석해 보았다.
gcc -Oz -S test.c -o gcc_Oz.s

루프는 그대로 유지된 것을 볼 수 있으며, 동작은 -O2옵션을 사용했을 때와 유사하다.
-O2과 비교를 해보자 먼저 -O2

종료 조건을 두 번 확인하는 것을 볼 수 있다.
이번엔 -Oz옵션을 살펴보자

루프의 종료조건을 한 번 확인하는 것을 볼 수 있다.
그 외에도 add명령어 대신 inc명령어를 사용하거나 분기문을 최소화한 것을 볼 수 있었다.

시간 비교

time명령어를 통해 실행파일의 동작 시간을 확인할 수 있다.

  • real : 실제 경과 시간
  • user : 사용자 모드에서 소비된 CPU 시간
  • sys : 커널 모드에서 소비된 CPU 시간

명확한 시간 확인을 위해 while문의 반복 횟수 100,000,000으로 증가하여 진행해 보았다.

실행파일 생성 후

시간을 확인한 결과이다.
코드 실행시간이 많이 길지 않아 명확한 비교를 하긴 어렵다.

  • -O0이 가장 오래 걸린 것을 볼 수 있음
  • Ofast가 가장 빠름

크기 비교


코드의 길이가 길지 않고 복잡하지 않아 크기의 큰 변화는 보기 어렵다.
하지만 Ofast옵션의 크기가 가장 큰 것은 뚜렷하게 볼 수 있었다.

0개의 댓글