성능 튜닝

MeCab-Ko의 성능을 최적화하는 방법을 소개합니다.

벤치마크 기준

테스트 환경:

  • CPU: AMD Ryzen 9 5950X (16 cores)
  • RAM: 64GB DDR4-3200
  • OS: Ubuntu 22.04 LTS
  • Rust: 1.75.0

기준 성능:

  • 단일 스레드: ~15MB/s
  • 멀티 스레드 (16 cores): ~180MB/s
  • 메모리 사용량: ~120MB (사전 로딩 포함)

Tagger 설정 최적화

띄어쓰기 패널티 조정

#![allow(unused)]
fn main() {
use mecab_ko::{Tagger, TaggerConfig};

let mut config = TaggerConfig::default();

// 높은 패널티 = 더 많은 띄어쓰기 (빠름, 정확도 낮음)
config.space_penalty = -500;

// 낮은 패널티 = 적은 띄어쓰기 (느림, 정확도 높음)
config.space_penalty = -2000;

// 기본값: -1000 (균형)
config.space_penalty = -1000;
}

성능 영향:

  • -500: 속도 +20%, 정확도 -5%
  • -1000: 기준
  • -2000: 속도 -15%, 정확도 +3%

최대 그룹화 크기

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();

// 작은 크기 = 빠름, 긴 복합어 처리 약함
config.max_grouping_size = 12;

// 큰 크기 = 느림, 긴 복합어 처리 강함
config.max_grouping_size = 48;

// 기본값: 24 (권장)
config.max_grouping_size = 24;
}

부분 처리 모드

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();

// 부분 처리 비활성화 (기본값, 더 빠름)
config.partial = false;

// 부분 처리 활성화 (미등록어 처리 강화, 느림)
config.partial = true;
}

Tagger 재사용

잘못된 사용

#![allow(unused)]
fn main() {
// Bad: 매번 Tagger 생성 (매우 느림)
for text in texts.iter() {
    let tagger = Tagger::new(TaggerConfig::default())?; // 비효율적!
    let result = tagger.parse(text)?;
}
}

성능: ~0.5MB/s

올바른 사용

#![allow(unused)]
fn main() {
// Good: Tagger 재사용
let tagger = Tagger::new(TaggerConfig::default())?;
for text in texts.iter() {
    let result = tagger.parse(text)?;
}
}

성능: ~15MB/s (30배 빠름)

병렬 처리

Rayon을 사용한 병렬화

#![allow(unused)]
fn main() {
use rayon::prelude::*;
use std::sync::Arc;

let tagger = Arc::new(Tagger::new(TaggerConfig::default())?);

let results: Vec<_> = texts
    .par_iter()
    .map(|text| {
        let tagger = Arc::clone(&tagger);
        tagger.parse(text)
    })
    .collect::<Result<_, _>>()?;
}

성능 (16 cores): ~180MB/s (12배 빠름)

수동 스레드 풀

#![allow(unused)]
fn main() {
use std::thread;
use std::sync::mpsc;

fn parallel_analyze(texts: Vec<String>, num_threads: usize) -> Vec<String> {
    let chunk_size = texts.len() / num_threads;
    let (tx, rx) = mpsc::channel();

    for chunk in texts.chunks(chunk_size) {
        let tx = tx.clone();
        let chunk = chunk.to_vec();

        thread::spawn(move || {
            let tagger = Tagger::new(TaggerConfig::default()).unwrap();
            for text in chunk {
                let result = tagger.parse(&text).unwrap();
                tx.send(result).unwrap();
            }
        });
    }

    drop(tx);
    rx.iter().collect()
}
}

배치 처리

내장 배치 API

#![allow(unused)]
fn main() {
let tagger = Tagger::new(TaggerConfig::default())?;

// 배치 처리 (더 효율적)
let results = tagger.parse_batch(&texts)?;
}

성능: +15% vs 개별 처리

커스텀 배치 크기

#![allow(unused)]
fn main() {
fn process_in_batches(
    tagger: &Tagger,
    texts: &[String],
    batch_size: usize,
) -> Result<Vec<String>, Error> {
    texts
        .chunks(batch_size)
        .flat_map(|batch| tagger.parse_batch(batch))
        .collect()
}

// 최적 배치 크기: 100-1000
let results = process_in_batches(&tagger, &texts, 500)?;
}

메모리 최적화

사전 메모리 매핑

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();
config.use_mmap = true; // 메모리 사용량 감소

let tagger = Tagger::new(config)?;
}

메모리 사용량:

  • 일반 로딩: ~120MB
  • mmap: ~20MB (6배 감소)

성능 영향: -5% (메모리 절약 우선시)

사전 압축

사전 빌드 시 압축 활성화:

mecab-ko-dict-build --compression --compression-level 6

사전 크기:

  • 비압축: ~250MB
  • 압축 (level 6): ~85MB (3배 감소)

로딩 시간:

  • 비압축: ~50ms
  • 압축: ~80ms

String Interning

반복되는 문자열을 인터닝:

#![allow(unused)]
fn main() {
use string_cache::DefaultAtom as Atom;

let mut seen = HashMap::new();

for node in nodes {
    let surface = seen
        .entry(node.surface.clone())
        .or_insert_with(|| Atom::from(node.surface.clone()));
}
}

메모리 절약: ~30% (대용량 데이터)

I/O 최적화

버퍼링

#![allow(unused)]
fn main() {
use std::io::{BufRead, BufReader};
use std::fs::File;

let file = File::open("large_file.txt")?;
let reader = BufReader::with_capacity(1024 * 1024, file); // 1MB 버퍼

for line in reader.lines() {
    let line = line?;
    let result = tagger.parse(&line)?;
}
}

성능: +40% vs 버퍼 없음

비동기 I/O (Tokio)

use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let file = File::open("large_file.txt").await?;
    let reader = BufReader::new(file);
    let mut lines = reader.lines();

    while let Some(line) = lines.next_line().await? {
        let result = tagger.parse(&line)?;
        // 처리
    }

    Ok(())
}

출력 포맷 최적화

Wakati 모드

품사 정보가 불필요한 경우:

#![allow(unused)]
fn main() {
let mut config = TaggerConfig::default();
config.output_format = OutputFormat::Wakati; // 빠름

let tagger = Tagger::new(config)?;
}

성능: +25% vs MeCab 포맷

커스텀 포맷

필요한 정보만 출력:

#![allow(unused)]
fn main() {
config.output_format = OutputFormat::Custom("%m\n".to_string());
}

성능: +30% vs 전체 feature 출력

프로파일링

CPU 프로파일링

# perf를 사용한 프로파일링
cargo build --release
perf record --call-graph dwarf ./target/release/mecab-ko large_file.txt
perf report

메모리 프로파일링

# heaptrack 사용
heaptrack ./target/release/mecab-ko large_file.txt
heaptrack_gui heaptrack.mecab-ko.*.gz

Flamegraph

cargo install flamegraph
cargo flamegraph --bin mecab-ko -- large_file.txt

컴파일 최적화

Release 프로필

Cargo.toml:

[profile.release]
opt-level = 3              # 최대 최적화
lto = "fat"                # Link-Time Optimization
codegen-units = 1          # 단일 코드 생성 유닛
panic = "abort"            # panic 시 abort (작은 바이너리)
strip = true               # 디버그 심볼 제거

바이너리 크기: 15MB → 8MB 성능: +10%

타겟 CPU 최적화

# 현재 CPU에 최적화
RUSTFLAGS="-C target-cpu=native" cargo build --release

성능: +5-15% (CPU 의존적)

PGO (Profile-Guided Optimization)

# 1. 계측 빌드
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release

# 2. 대표 워크로드 실행
./target/release/mecab-ko large_corpus.txt

# 3. PGO 빌드
llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --release

성능: +15-20%

벤치마크

Criterion 벤치마크

#![allow(unused)]
fn main() {
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use mecab_ko::{Tagger, TaggerConfig};

fn bench_parse(c: &mut Criterion) {
    let tagger = Tagger::new(TaggerConfig::default()).unwrap();
    let text = "형태소 분석 벤치마크 테스트입니다.";

    c.bench_function("parse", |b| {
        b.iter(|| tagger.parse(black_box(text)))
    });
}

criterion_group!(benches, bench_parse);
criterion_main!(benches);
}

실행:

cargo bench

처리량 측정

#![allow(unused)]
fn main() {
use std::time::Instant;

let tagger = Tagger::new(TaggerConfig::default())?;
let text = std::fs::read_to_string("large_corpus.txt")?;
let size = text.len();

let start = Instant::now();
tagger.parse(&text)?;
let elapsed = start.elapsed();

let throughput = size as f64 / elapsed.as_secs_f64() / 1_000_000.0;
println!("처리량: {:.2} MB/s", throughput);
}

실전 최적화 사례

웹 서버 최적화

use actix_web::{web, App, HttpServer};
use std::sync::Arc;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // 전역 Tagger (Arc로 공유)
    let tagger = Arc::new(Tagger::new(TaggerConfig::default()).unwrap());

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(tagger.clone()))
            .route("/analyze", web::post().to(analyze))
    })
    .workers(16) // CPU 코어 수
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

처리량: ~10,000 req/s

스트리밍 처리

#![allow(unused)]
fn main() {
use tokio::io::AsyncBufReadExt;

async fn stream_analyze(reader: impl AsyncBufRead + Unpin) {
    let tagger = Tagger::new(TaggerConfig::default()).unwrap();
    let mut lines = reader.lines();

    while let Some(line) = lines.next_line().await.unwrap() {
        if !line.trim().is_empty() {
            tokio::task::spawn_blocking({
                let tagger = tagger.clone();
                let line = line.clone();
                move || tagger.parse(&line)
            })
            .await
            .unwrap()
            .unwrap();
        }
    }
}
}

성능 체크리스트

  • Tagger 재사용 (매번 생성하지 않기)
  • 병렬 처리 활용 (Rayon, 스레드 풀)
  • 배치 처리 사용
  • 메모리 매핑 활성화 (대용량 사전)
  • 적절한 출력 포맷 선택
  • Release 프로필 최적화
  • 버퍼링 I/O 사용
  • 불필요한 feature 비활성화
  • PGO 적용 (중요 워크로드)
  • 프로파일링으로 병목 지점 확인

성능 비교

구현속도 (MB/s)메모리 (MB)
MeCab (C++)18150
MeCab-Ko (Rust)15120
Lindera12180
Kiwi22200

참고 자료