성능 튜닝 가이드

Vais 프로그램을 최적화하는 방법을 배웁니다. 컴파일 최적화, 벤치마킹, 프로파일링 기법을 다룹니다.

컴파일 최적화

최적화 수준 (Optimization Levels)

Vais 컴파일러는 LLVM을 기반으로 하여 여러 최적화 수준을 지원합니다:

# -O0: 최적화 없음 (기본값)
# - 빠른 컴파일
# - 큰 바이너리 크기
# - 느린 실행 속도
# - 디버깅 정보 유지
vaisc build -O0 myprogram.vais -o myprogram

# -O1: 기본 최적화
# - 균형잡힌 컴파일 시간과 성능
# - 중간 크기의 바이너리
vaisc build -O1 myprogram.vais -o myprogram

# -O2: 높은 최적화 (권장)
# - 우수한 성능
# - 합리적인 컴파일 시간
# - 더 큰 바이너리
vaisc build -O2 myprogram.vais -o myprogram

# -O3: 최대 최적화
# - 최고의 성능
# - 긴 컴파일 시간
# - 가장 큰 바이너리
# - 때로는 -O2보다 느릴 수 있음
vaisc build -O3 myprogram.vais -o myprogram

릴리스 빌드

프로덕션 배포를 위해 릴리스 빌드를 사용합니다:

# 릴리스 빌드 (자동으로 -O2 적용)
vaisc build --release myprogram.vais

# 추가 최적화 (LTO, 링크타임 최적화)
vaisc build --release --lto myprogram.vais

LTO는 전체 프로그램을 분석하여 추가 최적화를 수행합니다:

# Vais.toml
[profile.release]
lto = true           # 링크타임 최적화 활성화
codegen-units = 1   # 단일 코드 생성 단위 (더 느리지만 더 최적화됨)

프로필 유도 최적화 (PGO)

실제 프로그램 실행 데이터를 사용하여 최적화합니다:

# 1단계: 계측 빌드
vaisc build --pgo-generate myprogram.vais -o myprogram

# 2단계: 프로필 생성을 위해 프로그램 실행
./myprogram < input.txt

# 3단계: 최적화된 빌드
vaisc build --pgo-use myprogram.vais -o myprogram-optimized

벤치마킹

간단한 벤치마크

성능을 측정하기 위한 기본 벤치마크를 작성합니다:

# 현재 시간(나노초 단위) 반환
F get_nanotime() -> i64 {
    # 표준 라이브러리의 time 함수 사용
    0  # 실제 구현은 C 바인딩
}

# 벤치마크 실행 함수
F benchmark<F>(name: str, iterations: i64, f: F) {
    start := get_nanotime()

    ~ i := 0
    L i < iterations {
        f()
        i = i + 1
    }

    end := get_nanotime()
    elapsed := end - start

    avg_ns := elapsed / iterations
    puts("Benchmark {name}:")
    puts("  Total: {elapsed} ns")
    puts("  Avg: {avg_ns} ns/iteration")
}

# 테스트할 함수
F fibonacci(n: i64) -> i64 {
    I n <= 1 { R n }
    R fibonacci(n - 1) + fibonacci(n - 2)
}

F main() -> i64 {
    # 벤치마크 실행
    benchmark("fibonacci(20)", 100, || { fibonacci(20) })

    0
}

Criterion 벤치마크

더 정교한 벤치마킹을 위해 Criterion을 사용합니다:

# benches/fibonacci_bench.vais
F fibonacci(n: i64) -> i64 {
    I n <= 1 { R n }
    R fibonacci(n - 1) + fibonacci(n - 2)
}

F fibonacci_optimized(n: i64) -> i64 {
    I n <= 1 { R n }

    ~ a := 0
    ~ b := 1
    ~ i := 2

    L i <= n {
        ~ c := a + b
        a = b
        b = c
        i = i + 1
    }

    b
}

# 벤치마크 실행
# vaisc bench --bench fibonacci_bench

메모리 할당 벤치마크

메모리 할당 성능을 측정합니다:

S Node {
    value: i64
    next: i64  # 다음 노드 포인터
}

F create_linked_list(size: i64) -> i64 {
    ~ head := 0
    ~ i := 0

    L i < size {
        # 노드 생성 시뮬레이션
        node := Node { value: i, next: 0 }
        # 리스트에 추가
        i = i + 1
    }

    head
}

F benchmark_allocation() {
    puts("=== Memory Allocation Benchmark ===")

    # 작은 할당
    puts("Small allocations (1K):")
    start := get_time()
    create_linked_list(1000)
    elapsed := get_time() - start
    puts("Time: {elapsed} ms")

    # 큰 할당
    puts("Large allocations (10K):")
    start = get_time()
    create_linked_list(10000)
    elapsed = get_time() - start
    puts("Time: {elapsed} ms")
}

프로파일링

컴파일 시간 프로파일링

컴파일 성능을 분석합니다:

# 컴파일 시간 측정
time vaisc build --release myprogram.vais

# 자세한 타이밍 정보
vaisc build --release --timings myprogram.vais

런타임 프로파일링

실행 중인 프로그램의 성능을 분석합니다:

# Perf를 사용한 프로파일링 (Linux)
perf record -g ./myprogram
perf report

# Instruments를 사용 (macOS)
instruments -t "Time Profiler" ./myprogram

# Valgrind를 사용한 메모리 프로파일링
valgrind --tool=cachegrind ./myprogram

Flamegraph 생성

성능 분석을 위한 flamegraph를 생성합니다:

# Linux에서 flamegraph 생성
perf record -g -F 99 ./myprogram
perf script | stackcollapse-perf.pl | flamegraph.pl > perf.svg

# 결과 확인
open perf.svg

메모리 관리 최적화

스택 vs 힙

# 스택 할당 (빠름, 크기 제한)
F stack_example() {
    arr: [i64; 100] = [0; 100]  # 스택에 할당
    arr[0] = 42
}

# 힙 할당 (느림, 제한 없음)
F heap_example() {
    # 동적 할당 시뮬레이션
    ~ data := 42
}

# 스택 할당이 선호됨
F preferred_approach() {
    # 작은 고정 크기 배열은 스택에
    small_array: [i64; 10]

    # 큰 배열이나 동적 크기는 힙에
    # (라이브러리 함수 사용)
}

메모리 재사용

S Buffer {
    data: [i64; 1000]
    size: i64
}

# 버퍼를 재사용하여 할당 최소화
F process_multiple_batches(batches: i64) {
    buffer := Buffer { data: [0; 1000], size: 0 }

    ~ i := 0
    L i < batches {
        # 버퍼 내용 초기화 (재할당 아님)
        buffer.size = 0

        # 데이터 처리
        # ...

        i = i + 1
    }
}

GC 최적화

Vais의 선택적 GC를 최적화합니다:

# Vais.toml
[profile.release]
# GC를 사용하지 않도록 설정 (수동 관리)
gc = false

# 또는 GC 튜닝
gc = true
gc-threads = 4  # GC 스레드 수
gc-heap-size = "1GB"  # 초기 힙 크기

알고리즘 최적화

불필요한 연산 제거

# 비효율: 루프에서 계산 반복
F inefficient() {
    ~ sum := 0
    ~ i := 0
    L i < 1000 {
        # 루프마다 sin 계산
        sum = sum + sin(3.14159 / 2)
        i = i + 1
    }
}

# 효율: 루프 전에 계산
F efficient() {
    sin_value := sin(3.14159 / 2)  # 한 번만 계산
    ~ sum := 0
    ~ i := 0
    L i < 1000 {
        sum = sum + sin_value
        i = i + 1
    }
}

데이터 구조 선택

# 순차 접근이 많을 때: 배열 사용 (캐시 효율)
F array_approach() {
    arr := [1, 2, 3, 4, 5]
    ~ sum := 0
    ~ i := 0
    L i < 5 {
        sum = sum + arr[i]
        i = i + 1
    }
}

# 무작위 접근이 많을 때: 해시맵 고려
# (표준 라이브러리에서 HashMap 제공)

루프 최적화

# 비효율: 의존성이 있는 연산
F inefficient_loop() {
    ~ result := 0
    ~ i := 0
    L i < 1000 {
        result = result + (i * i)  # 각 반복이 이전 결과에 의존
        i = i + 1
    }
}

# 효율: 병렬화 가능한 연산
F efficient_loop() {
    ~ sum := 0
    ~ i := 0
    L i < 1000 {
        sum = sum + (i * i)  # 같은 연산이지만 더 효율적
        i = i + 1
    }
}

SIMD 활용

SIMD(Single Instruction Multiple Data)를 사용하여 벡터 연산을 가속화합니다:

# SIMD 타입 (아키텍처에 따라 다름)
# i64x4, f64x4 등

F vector_add(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
    # 일반적인 루프
    ~ result: [f64; 4]
    ~ i := 0
    L i < 4 {
        result[i] = a[i] + b[i]
        i = i + 1
    }
    result
}

# SIMD 버전 (컴파일러가 최적화)
F vector_add_simd(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
    # 벡터 연산 (컴파일러가 SIMD로 변환)
    a  # 간단한 예: 직접 반환
}

GPU 코드 생성

집약적인 계산을 GPU에서 실행합니다:

# GPU 최적화 빌드
vaisc build --gpu cuda myprogram.vais -o myprogram

# Metal (macOS)
vaisc build --gpu metal myprogram.vais -o myprogram

성능 최적화 체크리스트

□ 적절한 최적화 수준 선택 (-O2 권장)
□ LTO 활성화 (릴리스 빌드)
□ 불필요한 메모리 할당 제거
□ 스택 메모리 선호
□ 캐시 친화적 데이터 구조 사용
□ 루프 최적화 (의존성 제거)
□ 벤치마킹으로 병목 지점 식별
□ 프로파일링으로 실제 성능 측정
□ SIMD 활용 가능성 검토
□ GPU 오프로딩 검토 (큰 계산 작업)
□ 알고리즘 복잡도 분석 (Big-O)

실제 최적화 예제

최적화 전후 비교

# 비효율: 반복되는 계산과 할당
F naive_prime_check(n: i64) -> bool {
    ~ i := 2
    L i < n {
        I n % i == 0 { R false }
        i = i + 1
    }
    true
}

# 최적화: 불필요한 연산 제거
F optimized_prime_check(n: i64) -> bool {
    I n <= 1 { R false }
    I n == 2 { R true }
    I n % 2 == 0 { R false }

    # sqrt(n)까지만 확인
    ~ i := 3
    L i * i <= n {
        I n % i == 0 { R false }
        i = i + 2  # 짝수 생략
    }
    true
}

F main() -> i64 {
    # 최적화된 버전이 훨씬 빠름
    benchmark("naive", 10000, || { naive_prime_check(1000) })
    benchmark("optimized", 10000, || { optimized_prime_check(1000) })

    0
}

다음 단계