https://blog.hybrid3d.dev/2020-03-08-differences-between-pow-and-2-2

이 글에선 수학 함수의 사용에 대한 주의점을 다루려고 한다. pow 함수를 통해 실제 문제가 되는 사례를 중심으로 수학 함수를 다룰 때 함수가 정의하는 영역(정의역/공역)을 다루는 것이 중요하다는 것을 알아본다.

pow(x, 2)와 x의 제곱

pow(x, y)은 x의 y 승을 나타내는 수학 함수다. 가령 x 와 y가 2라면 pow(2, 2)는 4를 나타내고, $2^2$도 4를 나타내니 이 둘은 같다고 할 수 있다. 하지만 pow 함수는 그렇게 호락호락한 함수가 아니라서 주의를 필요로 한다. 예를 들어 -2의 제곱을 계산하려고 한다면 어떨까?

$(-2)^2$은 4고 직관적으로라면 컴퓨터는 이 정도의 계산은 무리 없이 해야 한다. 하지만 그렇지 않다.

수학적으로 x의 2승은 $x \times x$ 과 같다. 만약 pow(-2, 2)를 수행하는 컴파일러가 2승이 정수라는 것을 눈치채고 똑똑하게도 $(-2) \times (-2)$로 바꿔준다면 아무런 문제가 없다. 하지만 컴파일러가 그렇지 못하다면(이러한 동작 방식은 컴파일러 마다 다르다) 이 계산은 조금 복잡해지고 결과적으로 NaN(Not a Number)가 된다. 알다시피 NaN이 나오면 그 계산은 오염되어 그 이후의 계산이 모두 망가진다.

그럼 왜 pow(-2, 2)가 NaN이 되는 것일까? 결론부터 말하자면 pow의 x는 음수를 입력으로 받지 않는다는 것이고 음수를 입력으로 받지 않는 건 pow 함수의 계산 방식 때문에 그렇다.

한계

당연한 말이지만 컴퓨터는 덧셈 하나도 마음껏 하지 못한다. 일반적으로 어느 이상 숫자가 올라가 해당 자료형이 담을 수 있는 숫자를 넘기면 오버플로우(overflow)가 발생한다. 뺄셈도 마찬가지고, 곱셈, 나눗셈도 각자의 여러 난항이 있다.

그래도 사칙 연산 정도는 CPU/GPU의 어셈블리 단에서 명령어 처리가 가능하다. 그 명령어 처리가 어떻게 가능한지는 흔히 말하는 논리 회로의 영역이다. 하지만 현실적으로 논리 회로로 모든 수학 함수를 다 나타낼 순 없다. 그중에서도 pow 함수는 의외로 다루는 영역(정의역/공역)이 넓은 편인데다 허수가 나오기도 하여 그렇게 쉽게 계산 할 수 있는 것은 아니다.

pow 함수는 일반적으로는 아래의 공식을 따른다.

$$ x^y = 2^{y~log_2x} $$

복잡한 수식이 나와버린 듯 하지만 컴퓨터가 발전하면서 $2^x$를 구하는 함수, $log_2 x$를 구하는 함수 정도는 어셈블리 명령어로 만들어 두었기 때문에 곱하기, $2^x$, $log_2 x$를 이용하면 일반적인 pow 함수를 계산할 수 있다.

$$ x^y = 2^{y~log_2x} $$

이 공식에는 원래 의도와는 다른 제약이 들어간다. $2^x$ 의 경우 모든 실수를 입력으로 받지만 log 함수의 x의 정의역은 실수 전체가 아니라 0보다 큰 x이다. x가 0이거나 0보다 작은 경우는 정의되지 않아서 넣으면 안된다[2]. 그러한 입력에는 NaN가 출력 되는 것이다.

하지만 x가 0이고 y 가 0보다 큰 경우의 $x^y$는 수학적으로 0이라서 NaN으로 두기 아깝다. 이런 경우는 분기문을 이용하면 어렵지 않게 처리할 수 있다. 실제로 pow 함수 내부에서는 분기 처리로 이러한 문제를 우회한다. 이러한 상황은 HLSL(High Level Shader Language)의 pow 함수의 스펙을 살펴보면 좀 더 명확해진다(참고 1).