Go 경진대회용 제로 할당 I/O: 6가지 엔지니어링 솔루션
경진대회에서 Go 개발자들은 표준 I/O 도구의 한계에 자주 부딪힙니다. fmt.Fscan은 매 호출마다 메모리를 할당하고, bufio.Scanner는 스캔 모드에 갇혀 있으며, bufio.Reader로 수동 파싱하면 엄청난 보일러플레이트 코드를 작성해야 합니다. contestio 라이브러리는 bufio.Reader의 내부 버퍼를 직접 활용해 모든 작업에서 제로 할당을 실현합니다.
수동 파싱의 속도, fmt의 편의성, bufio의 유연성을 결합한 6가지 핵심 솔루션입니다. 숫자, 문자열, 심지어 문자 단위 파싱 등 혼합 입력을 모드 전환 없이 처리합니다.
직접 버퍼 접근: 복사 없는 _nextToken
핵심은 _nextToken 함수로, bufio.Reader 내부 버퍼에서 바이트 슬라이스를 반환합니다. 할당이나 복사가 전혀 없습니다. 여전히 ReadString, Peek, ReadByte 같은 bufio.Reader 메서드를 사용할 수 있습니다.
var age int
var name string
ScanInt(br, &age) // contestio 활용
name, _ = br.ReadString('\n') // 표준 메서드
name = strings.TrimSpace(name)
고수준 ScanInt와 ScanWord는 저수준 호출과 잘 어울립니다. 딱딱한 "스캔 모드"가 없어 원하는 순서로 데이터를 읽을 수 있습니다.
제네릭으로 구현한 범용 정수 파서
하나의 제네릭 _parseInt[T Int]가 int8, uint16, int64, uint64를 모두 처리합니다. 컴파일러가 각 타입별 최적화된 코드를 생성합니다:
- 부호 없는 타입은 부호 처리 생략.
- 범위 상수 내장 (
absMin = 1<<7forint8). - 최대 20자리 디지털 루프,
ParseUint로 폴백.
func _parseInt[T Int](T, error) {
unsigned := ^T(0) >= 0
// 부호 처리, 디지털 루프, 범위 검사
}
어셈블리에서 불필요한 검사 없이 최적화된 코드가 확인됩니다. 성능 저하 없는 다재다능함입니다.
제로 할당 출력: 동적 데이터용 스크래치 버퍼
출력을 위해 bufio.Writer에 scratch [32]byte 배열을 붙입니다. 메인 버퍼가 꽉 차면 스크래치 버퍼를 채운 후 단일 Write로 플러시합니다.
func _writeAppendFunc(bw *Writer, appendVal func([]byte, T) []byte, v T) error {
var buf []byte
if bw.Available() < len(bw.scratch) {
buf = bw.scratch[:0]
} else {
buf = bw.AvailableBuffer()
}
buf = appendVal(buf, v)
_, err := bw.Write(buf)
return err
}
결과: 지원 타입 모두 제로 할당 출력.
경량 리플렉션으로 모든 타입 지원
ScanInt[T], ScanFloat[T], ScanWord[T] 같은 타입화된 함수가 메인 API입니다. 유연성을 위해 ScanAny(br, &product, &price)는 reflect.Kind별 파서 테이블을 사용합니다:
var _parseAnyTab = []_parseAnyFunc{
reflect.Int: _parseIntToAny[int],
reflect.Int8: _parseIntToAny[int8],
reflect.Float32: _parseFloatToAny[float32],
reflect.String: _parseWordToAny[string],
}
리플렉션은 단순 디스패치 역할: 인자 타입 확인 후 적합한 파서 호출. 루프 밖 재사용 변수 포인터 전달—할당 없음.
var x int
for i := 0; i < N; i++ {
x = data[i]
PrintAny(bw, op, &x)
}
빌드 태그로 must: 패닉 또는 에러 처리
경진대회 입력은 대체로 깨끗하지만, 검사는 필수입니다. 매 호출마다 if err != nil { panic(err) } 생략—비공개 _must 함수 사용.
func _scanSlice[T any](int, error) {
return _must(_scanSliceCommon(br, parse, a))
}
must.go (//go:build must)에서 _must는 io.EOF 외 에러 시 패닉. nomust.go는 error 반환. 컴파일러가 적합 버전 인라인.
go run -tags=must main.go # 패닉
go run main.go # 에러 반환
인라인 유틸리티로 독립 솔루션
Codeforces 같은 플랫폼은 외부 패키지 금지. contestio-inline은 AST 분석 후 사용된 contestio 함수만 main.go에 임베드합니다.
contestio-inline main.go # 임베드
contestio-inline -clear main.go # 복원
정확한 임포트 import . "github.com/aaa2ppp/contestio" 필요. 컴파일 검사, 파일 백업, -tags=must 지원.
인라인 이점:
- 의존성 없음.
- 필요한 코드만 (의존 그래프).
- 솔루션 코드 그대로.
벤치마크: 100만 숫자, 4KB 버퍼
| Method | Read Time, ms | Allocations | Write Time, ms | Allocations |
|--------|---------------|-------------|----------------|-------------|
| fmt.Fscan/Fprint | 473 | 1 005 000 | 109 | 965 000 |
| Scanner+Atoi | 106 | 0 | - | - |
| strconv.AppendInt | - | - | 37 | 2 600 |
| contestio | 62 | 0 | 39 | 0 |
테스트 환경: Intel Core i3-7100, Go 1.24.2, Windows.
핵심 요약
- 제로 할당 읽기/쓰기: 직접 버퍼 접근.
- 제네릭으로 정수 타입별 최적 코드 생성.
- 스크래치 버퍼로 할당 없는 출력.
- 리플렉션은 디스패치 전용, 직렬화 아님.
must빌드 태그로 에러 검사 보일러플레이트 제거.
— Editorial Team
아직 댓글이 없습니다.