본문으로 건너뛰기
김신건의 로그

Barrett Reduction

· 수정 · 📖 약 2분 · 939자/단어 #algorithm #optimization #modular-arithmetic #barrett-reduction #constant-factor
Barrett Reduction, 바렛 리덕션

정의

Barrett Reduction고정된 modulus m 에 대해 x mod m나눗셈 명령 없이 곱셈 + 시프트 + 뺄셈 몇 번으로 계산하는 알고리즘. Paul Barrett 1986.

CPU 의 정수 나눗셈은 덧셈 / 곱셈에 비해 20 ~ 30 배 느림. NTT 의 핫루프, 모듈러 거듭제곱, 모듈러 행렬 곱 같은 수십억 회 mod 연산이 일어나는 코드 에서 큰 차이.

문제 상황과 동기

x86/ARM 에서 정수 나눗셈 명령 (div/idiv) 의 레이턴시는 곱셈의 20~30 배. 예를 들어 현대 x86 의 mul 은 ~3 사이클, div 는 ~20-30 사이클.

NTT butterfly 의 inner loop, 모듈러 거듭제곱, 큰 행렬의 mod 곱셈처럼 같은 modulus m 에 대해 수억~수십억 번 mod 연산 을 돌리는 경우, div 명령 하나가 전체 병목이 된다.

핵심 아이디어: modulus m 이 고정이면 floor(x / m)사전 계산된 곱셈 상수와의 곱셈 + 비트 시프트 로 근사할 수 있다. 정확도 오차가 최대 1 이하이므로 한 번의 조건 뺄셈으로 정확한 나머지를 얻는다.

이 트릭으로 NTT 벤치마크에서 2~4 배 실측 속도 향상. PS 에서 TLE 를 통과시키는 핵심 최적화.

시각화

핵심 아이디어

x mod m = x - floor(x / m) * m. 나눗셈을 역원의 곱셈 으로 대체.

사전 계산: M = floor(2^k / m)         (한 번만)

x mod m 계산:
    q = (x * M) >> k       (대략적인 몫)
    r = x - q * m
    if r >= m: r -= m
    return r

k 는 보통 64 또는 2 * bit_length(m). 곱셈 결과가 128 비트일 수 있으므로 __int128 사용.

정확도 보장

M = floor(2^k / m) 이면 (x * M) >> kfloor(x / m) 의 근사값 q 를 준다. q 의 오차는 최대 1 이하이므로

r = x - q * m

[0, 2m) 범위. 한 번의 if r >= m: r -= m 로 정확한 나머지.

Step trace (작은 예)

m = 7, k = 64
M = floor(2^64 / 7) ≈ 2635249153387078802

x = 50 을 mod 7:
    z = 50 * M ≈ (50 * 2635249153387078802) / 2^64 ≈ 7.14...
    q = floor(z) = 7
    r = 50 - 7*7 = 1
    1 < 7 이므로 return 1

실제 50 mod 7 = 1. 정확.

구현

O(1) Barrett reduction, NTT 환경에서 ~2배 빠름

// Barrett Reduction, O(1) per reduce (실측 ~5 사이클 vs div ~20-30 사이클)
// 사전 계산: 생성자에서 magic number 계산
// reduce(x): x mod m 을 나눗셈 없이 계산
#include <cstdint>

struct Barrett {
    uint64_t m, im;   // modulus, magic
    Barrett(uint64_t m_) : m(m_), im(uint64_t(-1) / m_ + 1) {}
    
    uint32_t reduce(uint64_t x) const {
        // q ≈ x / m, 128비트 곱셈으로 근사
        unsigned __int128 z = (unsigned __int128)x * im;
        uint64_t q = (uint64_t)(z >> 64);
        uint64_t r = x - q * m;
        if (r >= m) r -= m;   // 오차 최대 1 보정
        return (uint32_t)r;
    }
};

// modint wrapper: 산술 연산자 오버로드로 편리하게
struct modint_barrett {
    uint32_t v;
    static Barrett bt;
    modint_barrett(uint64_t x = 0) : v(bt.reduce(x)) {}
    
    modint_barrett operator+(modint_barrett b) const {
        uint64_t s = v + b.v;
        return modint_barrett(s >= bt.m ? s - bt.m : s);
    }
    modint_barrett operator*(modint_barrett b) const {
        return modint_barrett(bt.reduce((uint64_t)v * b.v));
    }
    // -, /, pow 등 생략
};
Barrett modint_barrett::bt(998244353);

// NTT butterfly inner loop 에서의 사용 예 (vs naive mod)
// naive mod (div 명령): ~20-30 사이클/iteration
// Barrett mod: ~5-8 사이클/iteration, 실측 2~3배 빠름
void ntt_butterfly_example(modint_barrett* a, int len, uint64_t w) {
    modint_barrett wn = 1;
    for (int j = 0; j < len / 2; j++) {
        auto u = a[j];
        auto v = a[j + len / 2] * wn;
        a[j] = u + v;
        a[j + len / 2] = u + modint_barrett(modint_barrett::bt.m - v.v);
        wn = wn * modint_barrett(w);
    }
}

reduce 연산 = 128비트 곱셈 1 + 64비트 시프트 1 + 뺄셈 1 + 조건 뺄셈 1. 총 ~5 사이클.

Montgomery Reduction 과의 비교

같은 정신의 다른 기법. 가산 / 곱셈을 Montgomery 형식 으로 작업 후 마지막에만 원래 형식으로.

항목BarrettMontgomery
사전 계산M = 2^k / mR, R⁻¹
가산변환 없이변환 필요
곱셈표준Montgomery mul
코드 단순중간복잡
속도빠름더 빠름 (반복적 mod 곱셈 환경)

PS 에서는 Barrett 이 도입이 쉬워 더 자주.

응용

1. NTT 핫루프

butterfly 의 inner mod 곱셈이 가장 빈번. Barrett 으로 2 배 빠름.

2. 모듈러 거듭제곱

거듭제곱 a^n mod m 의 inner mod 연산.

3. 모듈러 행렬 곱

O(N³) mod 곱셈 + mod 가산. 큰 N 에 mod 가 핫스팟.

4. 큰 정수 라이브러리

GMP / NumPy 의 내부 mod 연산.

복잡도

작업비용
사전 계산O(1)
reduce 1 회O(1) (실측 ~5 사이클)
표준 mod (div) 1 회O(1) (실측 ~20-30 사이클)

Barrett 이 2 ~ 4 배 실측 빠름. 큰 차이.

함정

1. compile-time 알려진 mod

x % 998244353 같이 컴파일러가 컴파일 시 mod 를 알면 자체적으로 magic number 곱셈을 사용 (자동 Barrett 과 비슷). 런타임에만 정해지는 mod 에 진가.

2. 128 비트 곱

__int128 또는 _umul128 (MSVC). 구식 컴파일러는 지원 안 함.

3. 음수 처리

x 가 음수일 가능성이 있으면 양수로 정규화 후 Barrett.

4. AtCoder Library 등에 내장

atcoder::modint 같은 표준 라이브러리는 Barrett 또는 Montgomery 를 내장. 직접 구현보다 라이브러리 사용이 안전.

BOJ 연습 문제

번호제목링크
BOJ 17467N! mod P (2)kokoa-lab

참고

이 글의 용어 (4개)
Bitset Optimizationalgorithm
정의 Bitset Optimization 은 불리언 / 비트 단위 정보 를 또는 배열에 패킹해, 한 명령어로 64 비트씩 병렬 연산 함으로써 시간 복잡도를 /64 (또는 , w …
Fast I/Oalgorithm
정의 Fast I/O 는 표준 라이브러리의 일반 목적 입출력 (C 의 / , C++ 의 / , Python 의 ) 대신, 버퍼와 바이트 단위 처리에 특화된 직접 구현으로 상수항을…
FFT, NTTalgorithm
정의 FFT (Fast Fourier Transform, 고속 푸리에 변환) 은 길이 인 수열의 이산 푸리에 변환 (DFT) 을 O(n log n) 에 계산하는 분할정복 알고리즘…
SIMDalgorithm
정의 SIMD (Single Instruction Multiple Data) 는 한 명령어로 여러 데이터 를 병렬 처리하는 CPU 기능. x86 의 SSE / AVX / AVX2…

이 개념을 다룬 위키 페이지 (4)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기