Skip to main content

Command Palette

Search for a command to run...

컴파일러 최적화 (1) 상수: 상수 폴딩과 함수 인라이닝

Updated
컴파일러 최적화 (1) 상수: 상수 폴딩과 함수 인라이닝
M

Hello, I am Korean. Welcome, visitor. You are very cool. 안녕하세요, 저는 한국어입니다. 방문자여 환영한다. 당신은 매우 시원해.

고수준과 저수준

  • 고수준(High level): 컴퓨터 구조에서 고수준이란, 사람에게 가까운 영역을 뜻합니다.

  • 저수준(Low level): 컴퓨터 구조에서 저수준이란, 기계에게 가까운 영역을 뜻합니다. 아래 그림의 계층적인 그림에서 기반이 되는 영역, 즉 기저에 있는 영역들이 저수준이에요.

컴퓨터 구조론에서는 아래 그림처럼 기반 시스템에 가까울수록 저수준, 그 위에 돌아가는 프로그램들은 고수준으로 표현합니다.

하이레벨과 로우레벨로 표현하지만 이는 학습 난이도를 표현하는 개념이 아니라, 기반 시스템과 그 위에 쌓이는 것들의 각 계층을 구분하는 표현입니다.

저수준

  • 하드웨어: 이때 하드웨어는 반도체로 가득한 기계 덩어리를 말한다고 볼 수 있습니다.

  • 펌웨어: 그 기계에 있는 각종 소자 등의 제어를 할 수 있는 가장 기본 동작을 담은 프로그램입니다.

  • 운영체제: 하드웨어와 펌웨어 위에 올라가는 시스템 소프트웨어로, 우리가 흔히 알고 있는 윈도우, 맥, 리눅스 등이 운영체제에 해당합니다. 우리가 컴퓨터를 생각할 때 여기까지를 한 덩어리로 보는 것이고, 여기까지가 기반 시스템, 즉 플랫폼이라고 부를 수 있는 계층입니다.

고수준

  • 응용 프로그램: 메모장, 터미널, 탐색기, 크롬, 인텔리제이, VS Code, 이클립스, 카카오톡, 그림판 등 우리가 흔히 사용하는 프로그램은 대부분 응용 프로그램입니다. 시스템 소프트웨어와 구분해서 부르는 표현입니다. (소프트웨어는 시스템 소프트웨어와 응용 소프트웨어로 분류합니다.)

컴파일

컴파일(compile)은 사람이 작성한 소스 코드를 기계어처럼 컴퓨터가 바로 알 수 있는 명령으로 바꾸는 동작을 뜻하는 개념이었습니다. 이렇게 컴파일을 수행해 주는 도구가 '컴파일러(compiler)'입니다.

  • 넓은 의미: 어떤 언어를 다른 언어로 변환하는 것입니다. 요즘은 이렇게 넓은 의미로 사용합니다.

    • 예를 들어, 타입스크립트를 자바스크립트로 컴파일합니다.

    • 예를 들어, SCSS를 CSS로 컴파일합니다.

    • 보통 동수준 또는 저수준으로 변환하는 방향일 때 컴파일, 저수준에서 고수준으로 변환할 때는 디컴파일이라고 부릅니다.

  • 좁은 의미: 고급 언어를 저급 언어(또는 저수준에 가까운 언어)로 변환하는 것입니다.

    • 고급 언어(고수준 언어)는 사람이 이해하기 쉬운 언어입니다.

    • 저급 언어(저수준 언어)는 기계가 이해하기 쉬운 언어입니다. 좁은 의미로는 프로세서가 바로 이해할 수 있는 언어여야 하기 때문에, 기계어와 어셈블리어가 저급언어에 해당합니다.

따라서 우리가 프로그램을 만들고 실행하는 순서를 요약하면 다음과 같아요. (전통적인 프로세스)

  1. 사람이 프로그램 소스 코드를 작성합니다.

  2. 컴파일러가 이 소스 코드를 해석해서 기계가 알아먹을 수 있는 표현으로 바꿉니다.

  3. 운영체제가 프로그램을 작동합니다.

컴파일러의 최적화

컴파일러의 주요 기능

  • 역할 1: 컴파일러는 입력받은 프로그램(= 명령어들)의 의미를 보존하여 번역해야 합니다.

  • 역할 2: 컴파일러는 입력받은 프로그램을 실용적으로 개선합니다.

현대적인 컴파일러는 프로그래밍 언어의 번역 외에도 '최적화(optimization)' 기능을 탑재합니다.

우리가 작성한 코드(가독성 위주) → 최적화 → 최적화된 코드(성능 위주)


💡 최초의 컴파일러와 최초의 '완전한' 컴파일러

최초의 컴파일러는 1952년 그레이스 호퍼가 만든 A-0 시스템입니다. A-0 시스템의 역할은 기계어 작성을 단순화하기 위한 자동화 도구에 가까웠습니다. 사용자는 A-0 시스템에 특정 키워드로 작성된 명령어와 내장 라이브러리의 서브루틴(= 함수)을 사용하여 사람들에게 친화적인 고수준 수학 공식을 입력합니다. A-0 시스템은 입력받은 고수준 수학 공식을 머신 코드로 변환하는 자동화 도구였습니다. 단순화한 특정 명령어를 사용하고 라이브러리의 서브루틴을 호출하는 점에서 오늘날 고급 프로그래밍 언어에 영감을 준 시조격입니다.

최초의 완전한 컴파일러는 1957년 IBM의 존 베커스가 만든 포트란 컴파일러입니다. 이 컴파일러는 최적화 기능을 탑재한 완전한 컴파일러였습니다. 고급 프로그래밍 언어 중 최초로 대인기를 끌었다고 인정받는 포트란은, 그 컴파일러가 언어를 번역하는 기능뿐 아니라 최적화하는 기능을 수행함으로써 현대적인 컴파일러의 주요 역할을 충족하여 최초의 완전한 컴파일러로 봅니다.


최적화를 위한 상수 사용

💡 상수를 사용하면 일부가 컴파일타임에 해석되어 최적화됩니다.

컴파일러는 자신이 예측하기 쉬운 것들은 쉽게 최적화할 수 있습니다. 프로그래밍에서 상수 사용은 값의 불변성을 보장하기 때문에, 예측하기 쉬운 상태가 됩니다.

상수 폴딩

상수 폴딩은 값을 미리 계산할 수 있는 것들은 컴파일타임에 미리 계산해 준다는 최적화 개념입니다.

기호 상수를 사용하면 상수 전파도 적용될 수 있습니다. 기호 상수의 값을 컴파일타임에도 알 수 있을 때, 기호 상수를 리터럴 상수로 대체합니다. 이것이 상수 전파(constant propagation)입니다.

상수 전파 후 상수 폴딩으로 간단한 수식 정도는 컴파일타임에 계산해서 결과를 넣습니다.

상수 폴딩 전

public class Example {

    private static final int GAIN = 1024;

    public static final int getCalculatedValue() {
        int a = 10;
        int b = 2 * a + 1;
        return (a * b) + GAIN;
    }
}

상수 폴딩 후

컴파일타임에 GAIN을 알기 때문에 상수 전파가 되고, 식이 모두 계산되어 결과만 남깁니다. 다른 값들은 리터럴 상수를 넣은 지역변수이기 때문에 컴파일러가 미리 계산할 수 있습니다.

// 컴파일 타임 이후 원래 바이트코드 등으로 표현되지만 자바 코드로 봅시다.
public class Example {

    private static final int GAIN = 1024;

    public static final int getCalculatedValue() {
        return 1234;
    }
}

함수 인라이닝

함수를 호출하는 비용도 아낄 수 있도록 함수의 동작을 그대로 함수 호출부로 넣는 작업입니다.

예를 들어 메서드가 상수를 반환할 때는 메서드 호출 대신 그 상수를 넣을 수 있습니다.

인라이닝 전 (위 예시의 함수를 사용함)

public class Main {
    public static void main() {
        System.out.println(Example.getCalculatedValue());
    }
}

인라이닝 후

쉬운 이해를 위해 인라이닝 후 상수 폴딩을 적용했습니다.

public class Main {
    public static void main() {
        System.out.println(1234);
    }
}

상수 전파, 상수 폴딩, 함수 인라이닝의 순서

  • 일부 상수는 함수를 통해 제공되고 있을 수 있습니다. (함수 인라이닝 필요)

  • 일부 함수는 상수 폴딩을 해야 최종 결과로 상수를 반환하는 것을 확인할 수 있습니다.

이처럼 최적화 기법이 서로 종속될 수 있기 때문에, 적용 순서에 따라 최적화 스텝이 달라질 수 있습니다.

(중략 ^^)

일반적으로 함수 인라이닝을 상수 폴딩보다 먼저 수행하는 것이 좋습니다. 함수 인라이닝을 통해 앞으로 필요한 최적화 요소를 같이 발견할 수 있기 때문입니다.

따라서 오늘 나온 개념을 컴파일타임에 적용할 때, 권장하는 순서는 다음과 같습니다.

  1. 함수 인라이닝

  2. 상수 전파

  3. 상수 폴딩

More from this blog

클래스에 Serializable 인터페이스를 구현 받는 이유가 무엇인가요? #42

이 아티클은 깃허브 nettee-space 조직의 디스커션 #42 항목을 옮겨 온 것입니다.관련 논의: nettee-space/backend-sample-hexagonal-simple-crud/discussions/42 Question: 클래스에 Serializable 인터페이스를 구현 받는 이유가 무엇인가요? 여러 소스들을 접하면서 VO 객체 등에 Serializable를 구현받는 것을 많이 접했습니다. 저희 헥사고날(스터디 팀내 2단계 ...

Feb 7, 2025
클래스에 Serializable 인터페이스를 구현 받는 이유가 무엇인가요? #42

개념 2. JWT 액세스 토큰의 생성과 전달, Stateful한 리프레시 토큰의 생성, 전달, 보존

이전 글에서 정리에 꽤 힘을 뺐기 때문에, 이번 글에서는 서두와 부연설명을 줄이고 필요한 정보를 담아 전달해 보겠습니다. JWT 액세스 토큰 JWT 액세스 토큰은 인가에 직접 사용되는 토큰이고, stateless 하다는 장점이 있었습니다. 액세스 토큰의 생성 JWT(JSON Web Token)로 생성합니다. 비밀번호 인증 등 자격 검토 후 JWT를 발급합니다. JWT는 헤더, 페이로드, 시그니처 세 영역을 점(.) 기호로 구분한다고 했습니다....

Oct 23, 2024
개념 2. JWT 액세스 토큰의 생성과 전달, Stateful한 리프레시 토큰의 생성, 전달, 보존

Merge Simpson의 매너 있고 다정한 한국어 개발자 블로그

37 posts