부동소수점(Floats)
부동소수점(Floats)
프로그램을 만들다 보면 정수만으로는 표현할 수 없는 값들, 예를 들어 실수나 아주 크거나 아주 작은 수를 다뤄야 할 때가 있어요. 그럴 때 Zig가 제시하는 해법이 바로 이번에 다룰 부동소수점 타입입니다. 어떤 타입이 있는지, 리터럴은 어떻게 쓰는지, 그리고 계산 시 부동소수점 연산이 어떻게 동작하는지까지 차근차근 살펴볼게요.
본문
Zig는 다음과 같은 부동소수점 타입들을 제공해요.
f16- IEEE-754-2008 binary16f32- IEEE-754-2008 binary32f64- IEEE-754-2008 binary64f80- IEEE-754-2008 80-bit extended precisionf128- IEEE-754-2008 binary128c_longdouble- matcheslong doublefor the target C ABI
숫자가 커질수록 더 많은 비트를 사용해서 더 넓은 범위와 정밀도를 담아요. 보통은 f32나 f64를 주로 쓰고, 필요에 따라 더 큰 타입을 고르면 됩니다.
Float Literals
부동소수점 리터럴은 comptime_float 타입을 가져요. 이 타입은 가장 큰 다른 부동소수점 타입인 f128과 동일한 정밀도와 연산을 갖도록 보장되어 있어요. 즉 리터럴은 어느 부동소수점 타입으로든 자연스럽게 변환될 수 있죠.
부동소수점 리터럴은 어떤 부동소수점 타입으로든 coerce할 수 있고, 소수 부분이 없는 경우에는 어떤 정수 타입으로도 변환할 수 있어요.
다양한 리터럴 표기법을 실제로 볼까요.
const floating_point = 123.0E+77;
const another_float = 123.0;
const yet_another = 123.0e+77;
const hex_floating_point = 0x103.70p-5;
const another_hex_float = 0x103.70;
const yet_another_hex_float = 0x103.70P-5;
// underscores may be placed between two digits as a visual separator
const lightspeed = 299_792_458.000_000;
const nanosecond = 0.000_000_001;
const more_hex = 0x1234_5678.9ABC_CDEFp-10;
눈여겨볼 점이 몇 가지 있어요. 일반적인 십진 표기(123.0)에 더해 지수 표기(123.0e+77)와 16진수 부동소수점(0x103.70p-5)도 지원하죠. 그리고 마지막 세 줄처럼 두 숫자 사이에 밑줄(_)을 넣어 가독성을 높일 수도 있어요.
다만 NaN, 무한대, 음의 무한대를 위한 문법은 없어요. 이런 특수한 값들은 표준 라이브러리를 통해 만들어야 합니다.
const std = @import("std");
const inf = std.math.inf(f32);
const negative_inf = -std.math.inf(f64);
const nan = std.math.nan(f128);
std.math의 inf와 nan 함수에 타입을 넘겨주는 방식이죠. 이렇게 해서 f32의 무한대, f64의 음의 무한대, f128의 NaN을 각각 얻을 수 있어요.
Floating Point Operations
기본적으로 부동소수점 연산은 .strict 모드를 사용해요. 하지만 블록 단위로 .optimized 모드로 전환할 수도 있습니다. 두 모드의 차이를 보여주는 예제를 봐볼게요.
const std = @import("std");
const big = @as(f64, 1 << 40);
export fn foo_strict(x: f64) f64 {
return x + big - big;
}
export fn foo_optimized(x: f64) f64 {
@setFloatMode(.optimized);
return x + big - big;
}
$ zig build-obj float_mode_obj.zig -O fast
두 함수 모두 x + big - big를 계산해요. big은 1을 40비트 왼쪽으로 시프트한 값이라 x에 비해 훨씬 커요. foo_strict는 그냥 기본 모드로 돌고, foo_optimized는 @setFloatMode(.optimized)로 최적화 모드를 켠 거죠.
여기서 코드를 두 개의 오브젝트 파일로 나눠야 하는 이유가 있어요. 이 테스트에서는 그렇지 않으면 최적화기가 모든 값을 컴파일 타임에 계산해 버리는데, 컴파일 타임 연산은 strict 모드로 동작하기 때문입니다.
const print = @import("std").debug.print;
extern fn foo_strict(x: f64) f64;
extern fn foo_optimized(x: f64) f64;
pub fn main() void {
const x = 0.001;
print("optimized = {}\n", .{foo_optimized(x)});
print("strict = {}\n", .{foo_strict(x)});
}
$ zig build-exe float_mode_exe.zig float_mode_obj.o -O fast $ ./float_mode_exe optimized = 0.001 strict = 0.0009765625
foo_strict와 foo_optimized를 extern으로 선언해 별도 오브젝트 파일의 함수를 가져와요. 실행 결과를 보면 x = 0.001일 때 optimized는 그대로 0.001을 반환하지만, strict는 0.0009765625가 나와요. x가 big보다 훨씬 작다 보니 x + big 결과에서 x의 정보가 사라져버리고, strict 모드는 그 연산 순서를 그대로 지키기 때문이죠. 반면 optimized 모드는 수식이 x로 정리된다는 걸 알아내고 더 정확한 결과를 냅니다.
더 알아보기
부동소수점 연산 모드와 관련된 내용은 아래 문서에서 더 자세히 다뤄요.