Rylah's Study & Daily Life

Modern C++ : 03. C++ Build : Assembly 본문

Study/C++

Modern C++ : 03. C++ Build : Assembly

Rylah 2022. 3. 14. 00:12
1
2
3
4
5
6
7
8
9
double multiply(double a, double b)
{
    return a * b;
}
 
double divide(double a, double b)
{
    return a / b;
}
cs

complier 옵션에 -S(대문자)를 주게 되면 .s 파일이 생성된다.

 

이 코드를 텍스트 편집기로 열어보면 다음과 같이 나오게 되고 이는 어셈블리어로 볼 수 있다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
    .file    "005.assembly.cpp"
    .text
    .globl    _Z8multiplydd
    .type    _Z8multiplydd, @function
_Z8multiplydd:
.LFB0:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6-16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movsd    %xmm0, -8(%rbp)
    movsd    %xmm1, -16(%rbp)
    movsd    -8(%rbp), %xmm0
    mulsd    -16(%rbp), %xmm0
    popq    %rbp
    .cfi_def_cfa 78
    ret
    .cfi_endproc
.LFE0:
    .size    _Z8multiplydd, .-_Z8multiplydd
    .globl    _Z6dividedd
    .type    _Z6dividedd, @function
_Z6dividedd:
.LFB1:
    .cfi_startproc
    endbr64
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6-16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movsd    %xmm0, -8(%rbp)
    movsd    %xmm1, -16(%rbp)
    movsd    -8(%rbp), %xmm0
    divsd    -16(%rbp), %xmm0
    popq    %rbp
    .cfi_def_cfa 78
    ret
    .cfi_endproc
.LFE1:
    .size    _Z6dividedd, .-_Z6dividedd
    .ident    "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long     1f - 0f
    .long     4f - 1f
    .long     5
0:
    .string     "GNU"
1:
    .align 8
    .long     0xc0000002
    .long     3f - 2f
2:
    .long     0x3
3:
    .align 8
4:
 
cs

이를 모두 읽어볼 필요는 없다.

 

multiplydd라는 함수에서 mulsd라는 것을 사용해서 연산 후 return 해준다는 것을 알 수 있다.

 

하지만 이는 매우 불편하다.

 

리눅스에서는 Complier Explorer로 이를 확인하기 쉽다.

 

https://godbolt.org/

 

Compiler Explorer

 

godbolt.org

 

컴파일러 익스플로러에서는 다양한 옵션 혹은 컴파일러를 통해 어셈블리어를 확인해볼 수 있다.

 

위 코드를 GCCx86-64에 넣어서 확인하면 다음과 같이 나온다.

 

컴파일러의 최적화를 확인해보자

 

1
2
3
4
5
6
7
8
9
10
unsigned int num8x(unsigned int a)
{   //multiply by 8
    return a * 8;
}
 
unsigned int num8x_2(unsigned int a)
{
    //multiply by 8
    return a << 3;
}
cs

 

예를 들어 다음과 같이 8을 곱하는 함수가 있다고 가정해보자.

 

첫 번째 함수는 말 그대로 8배를 하는 값을 return 하는 것이고, 두번째 함수는 bit shift로 3번 left shift 해주는 것으로 8배를 구현해준다.

 

몇몇 사람들은 비트 쉬프트 연산이 편리한게 아니라 더 속도가 좋다고 이야기하기도 한다.

 

과연 어셈블리어로는 어떻게 나올까?

 

gcc -O0로 컴파일 했을 때 나온 결과이다.

 

gcc -O2로 했을 때의 결과이다.

 

어떤 컴파일 옵션을 돌려도 어셈블리 코드로 동일하게 동작함을 알 수 있다.

 

두 함수는 차이가 없다. * 8로 짜는 것이 가독성이 더 좋을 것이다.

1
2
3
4
5
6
7
8
9
10
int divide (int a, int b)
{
    return a / b;
}
 
 
int divideBy13 (int a)
{
    return a / 13;
}
cs

그렇다면 나누기에 대해서 한번 보도록 해보자.

 

보통 나누기를 한다면 저렇게 함수를 짜게 될 것이다.

 

그리고 13으로 나누게 된다면 이렇게 짜야할 것이다.

 

어셈블리어는 어떨까?

 

위와 같이 차이가 나게 되는데 divideBy13이 길이가 더 길다고 느린거 아닌가요? 라고 물어볼 수 있다.

 

하지만 내부적으로 idiv라는 어셈블리어와 imul이라는 어셈블리어의 동작 속도 차이로 인해 아래 함수가 더 속도가 빠르다는 것을 알 수 있다. (약 5배 차이가 난다고 한다.)

 

 

이번에는 if-else와 switch문에서 어셈블리를 알아보자

 

코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int test(int a)
{
    if (a == 0)
        return a * 0;
    else if (a == 1)
        return a;
    else if (a == 3)
        return a / 4;
    else if (a == 4)
        return 5;
    return 0;
}
 
int test2(int a)
{
    switch (a)
    {
    case 0return a * 0;
    case 1return a;
    case 3return a / 4;
    case 4return 5;
    defaultreturn 0;
    }
}
cs

gcc로 컴파일 했을 때 어셈블리는 다음과 같다.

위가 if else이고 아래가 switch이다.

cmp 4까지 도달하는데 6개의 연산과 4개의 연산으로 switch 구문이 조금 더 연산이 빠르다는 것을 알 수 있다.

 

하지만 이것은 gcc 컴파일러에 대해서이다 다른 컴파일러는 어떨까?

clang 컴파일러에서는 완벽하게 동일하게 동작함을 알 수 있다.

MSVC(Visual Studio)에서는 다음과 같다.

cmp 연산은 if-else가 더 많음을 알 수 있다.

큰 차이는 없어보이지만 미세한 차이가 있는 것 같다.

 

 

clang을 사용하면 같은 것을 알 수 있듯이 무조건적으로 switch 문이 효율적이라고 말하기는 어려울 것이다.

 

정리

 * 2  * 4  / 2 / 4
 << 1  << 2  >> 1   >> 2

이와 같이 나타내는 경우에서 두 동작이 같음을 확인 할 수 있었다.

divide(a, b) divideBy13(a)
div Assembly use (코드는 짧으나 느림) mult를 이용해 구현(코드가 길지만 div 보다 빠름)

 

if else switch, case
컴파일러에 따라 다름 (clang에서는 동일) 컴파일러에 따라 다름 (clang에서는 동일)

 

가장 정확한 방법은 벤치마크를 돌려보는 것이 정확하다.

 

컴파일러에서는 가독성을 해치지 않게 짜도 어느정도 최적화 된 코드를 만들어주게 된다.

 

 

https://youtu.be/UrODm3337kE