본문 바로가기
LWN.net

[LWN.net] Shrinking the kernel with link-time optimization

by hyeyoo 2021. 11. 5.
※ 이 블로그의 글은 글쓴이가 공부하면서 정리하여 쓴 글입니다.
※ 최대한 내용을 검토하면서 글을 쓰지만 틀린 내용이 있을 수 있습니다.
※ 만약 틀린 부분이 있다면 댓글로 알려주세요.

 

 

Shrinking the kernel with link-time optimization [LWN.net]

Shrinking the kernel with link-time optimization This article brought to you by LWN subscribersSubscribers to LWN.net made this article — and everything that surrounds it — possible. If you appreciate our content, please buy a subscription and make the

lwn.net

Link Time Optimization

바로 앞 글에서는 가비지 컬렉션으로 코드를 줄이는 법을 알아봤다. 하지만 이 방법은 코드의 수정도 필요하고, 여러 모로 노력이 많이 들어간다. 이번 글에서는 더 강력하고 쉬운 방법인 LTO(Link Time Optimization)를 알아본다.

Dead-code Elimination

앞 글에서의 예제를 다시 가져와본다.

    int foo(void)  { return 1; }

    int bar(void)  { return foo() + 2; }

    int main(void) { return foo() + 4; }
        .text

        .type   foo, %function
    foo:
        mov     r0, #1
        bx      lr

        .type   bar, %function
    bar:
        push    {r3, lr}
        bl      foo
        adds    r0, r0, #2
        pop     {r3, pc}

        .type   main, %function
    main:
        push    {r3, lr}
        bl      foo
        adds    r0, r0, #4
        pop     {r3, pc}

위 코드에서 bar()는 실제로 호출되지 않음에도 불구하고 코드에 포함이 되었는데, 그건 컴파일 시점에서 bar()를 호출하는지를 알 수 없기 때문이다. 하지만 함수가 파일 내에서만 사용될 경우 static 함수로 만들면 파일 내에서만 호출이 되는지 확인하면 되므로 쉽게 최적화를 할 수 있다.

    static int foo(void) { return 1; }

    static int bar(void) { return foo() + 2; }

    int main(void)       { return foo() + 4; }
        .text

        .type   main, %function
    main:
        mov     r0, #5
        bx      lr

이때 bar()의 코드가 제거되었을 뿐만 아니라 foo()가 인라인 함수로 컴파일이 되어서 main()이 매우 간단해졌다. 그럼 여기서 질문을 해본다. 모든 소스코드를 하나의 소스로 합친 다음에 static을 붙이면 bar()처럼 최적화를 할 수 있지 않을까?

Link Time Optimization (LTO)

물론 현실적으로 소스코드를 하나의 파일로 관리할 수는 없다. 하지만 LTO를 활용해서 비슷한 원리로 최적화를 할 수 있다. 컴파일러에게 LTO 모드로 컴파일하라고 지시하면, 컴파일러는 모든 소스코드를 최적화없이 내부 표현으로 변환한다. 이렇게 하면 링커가 링크를 할 때 오브젝트 파일이 한 곳에 모이게 되므로 ㅡ 소스코드를 하나의 파일로 합친 것과 같은 상태가 된다 ㅡ 물론 합쳐진 것이 소스코드는 아니고 컴파일러의 내부 표현이지만 말이다. 이렇게사용되지 않는 코드를  쉽게 최적화할 수 있다.

 

한 번 foo(), bar(), main()의 예제를 LTO 모드로 컴파일해서 확인해보자. foo()와 bar()가 간단하게 제거된 것을 확인할 수 있다.

$ gcc -O2 -flto -c foo.c
$ gcc -O2 -flto -c bar.c
$ gcc -O2 -flto -c main.c
$ gcc -O2 -flto -o test foo.o bar.o main.o
$ nm test | grep "foo\|bar\|main"
000102c0 T main
$ objdump -d test
[...]
000102c0 <main>:
   102c0:       e3a00005        mov     r0, #5
   102c4:       e12fff1e        bx      lr

LTO and kernel

아쉽게도 메인라인 커널에는 LTO 패치가 머지되지 않았다. 이유는 메일링 리스트를 찾아봐야할 것 같다. 이 글에서는 Andi Kleen의 트리에서 (lto-415-2) 작업한다. 5.12-3 버전도 있다.

Numbers Please!

이제 구체적으로 LTO, Linker Section Garbage Collection이 얼마나 코드를 줄이는지 확인해보자. 우선 아무것도 적용하지 않았을 때를 보자.

$ make stm32_defconfig
$ make vmlinux
$ size vmlinux
   text    data     bss     dec     hex filename
1704024  144732  117660 1966416  1e0150 vmlinux

그리고 LTO를 활성화했을 때를 보자.

$ ./scripts/config --enable CONFIG_LTO_MENU
$ make vmlinux
$ size vmlinux
   text    data     bss     dec     hex filename
1281644  142492  112985 1537121  177461 vmlinux

크기가 약 22%정도 줄어든 것을 볼 수 있다. 이번에는 아까 설명했던 Linker Section Garbage Collection을 적용해보자.

$ [hacks for CONFIG_LD_DEAD_CODE_DATA_ELIMINATION]
$ make vmlinux
$ size vmlinux
   text    data     bss     dec     hex filename
1304516  141672  113108 1559296  17cb00 vmlinux

이것도 크기가 대략 21%정도 줄어든 것을 볼 수 있다. 하지만 아까 살펴봤듯 Garbage Collection은 노력이 많이 들어갈 뿐더러 디버깅과 같은 몇몇 기능을 사용할 수 없게 되기 때문에 LTO가 훨씬 효과적이고 많이 사용된다.

More Numbers

LTO는 바이너리의 크기를 효과적으로 줄여주는 대신 컴파일 시간을 더 많이 소모한다. 다음은 LTO를 비활성화 했을 때의 시간이다.

$ make clean
$ make stm32_defconfig
$ time make -j8 vmlinux

real    0m36.645s
user    3m59.252s
sys     0m21.026s

이번에는 LTO를 활성화했을 때의 시간이다.

$ make clean
$ ./scripts/config --enable CONFIG_LTO_MENU
$ time make -j8 vmlinux

real    1m24.774s
user    8m4.143s
sys     0m31.902s

컴파일에 시간을 더 많은 시간이 소요되는 것을 확인할 수 있다. 특히 최적화를 마지막에 하기 때문에 대기 시간이 늘어난 것을 볼 수 있다.

 

하지만 좀더 짜증날 수 있는 부분은 파일을 하나만 수정했을 때 다시 빌드를 하는 시간이다. LTO를 활성화하면 파일을 하나만 수정해도 최적화를 처음부터 다시해야한다.

 

LTO를 비활성화 했을때:

$ touch init/main.c
$ time make -j8 vmlinux

real    0m3.686s
user    0m5.803s
sys     0m1.819s

LTO를 활성화 했을때:

$ touch init/main.c
$ time make -j8 vmlinux

real    0m58.283s
user    5m6.089s
sys     0m12.732s

자, 여기까지는 LTO를 사용해서 크기를 약 22% 가량 줄이는 방법을 알아봤다. 하지만 이것도 아직 충분하지 않다. 다음섹션에서는 커널의 주요 기능들을 모듈로 분리해서 크기를 줄여본다.

댓글