제곱근 계산을 위한 함수인 pow(n, n)와 n**n 중 어느 게 더 빠를까?
결론부터 말하면 pow() 함수와 **는 "거의" 동일 합니다. 네 "거의"요
**의 경우 bignum을 통해 연산을 합니다, 반면 pow()는 부동소수점으로 변환되어 이 변환되는 과정이 있어 시간이 조금 더 소요된다고 합니다.
하지만 결국 power() 연산은 같은 함수를 하기 때문에 거의 비슷합니다.
여기 까지가 동작 과정의 대한 이야기인데... 실제 어떻게 구현되어 있는지 살짝만 보겠습니다.
cpython의 pow 계산하는 일부분만 가져왔습니다.
/* pow(v, w, x) */
static PyObject *
long_pow(PyObject *v, PyObject *w, PyObject *x)
{
PyLongObject *a, *b, *c; /* a,b,c = v,w,x */
int negativeOutput = 0; /* if x<0 return negative output */
PyLongObject *z = NULL; /* accumulated result */
Py_ssize_t i, j, k; /* counters */
PyLongObject *temp = NULL;
/* 5-ary values. If the exponent is large enough, table is
* precomputed so that table[i] == a**i % c for i in range(32).
*/
PyLongObject *table[32] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
/* a, b, c = v, w, x */
CHECK_BINOP(v, w);
a = (PyLongObject*)v; Py_INCREF(a);
b = (PyLongObject*)w; Py_INCREF(b);
....................................
if (Py_SIZE(b) <= FIVEARY_CUTOFF) {
/* Left-to-right binary exponentiation (HAC Algorithm 14.79) */
/* http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf */
for (i = Py_SIZE(b) - 1; i >= 0; --i) {
digit bi = b->ob_digit[i];
for (j = (digit)1 << (PyLong_SHIFT-1); j != 0; j >>= 1) {
MULT(z, z, z);
if (bi & j)
MULT(z, a, z);
}
}
}
else {
/* Left-to-right 5-ary exponentiation (HAC Algorithm 14.82) */
Py_INCREF(z); /* still holds 1L */
table[0] = z;
for (i = 1; i < 32; ++i)
MULT(table[i-1], a, table[i]);
for (i = Py_SIZE(b) - 1; i >= 0; --i) {
const digit bi = b->ob_digit[i];
for (j = PyLong_SHIFT - 5; j >= 0; j -= 5) {
const int index = (bi >> j) & 0x1f;
for (k = 0; k < 5; ++k)
MULT(z, z, z);
if (index)
MULT(z, table[index], z);
}
}
}
......................
return (PyObject *)z;
}
pow()는 cpython에서 Objects/longobject.c 에서 long_pow() 함수를 호출합니다.
여기에서 봐야 할 부분은 Left-to-right binary exponentiation입니다.
만일 저희가 기존 방식대로 3^65537을 한다면 3을 65537번 곱해야 하지만, 3을 65537번 곱하는 대신 65537이 2^16+1인 것을 착안해서 계산 속도를 높일 수 있습니다.
1. 3^2
2. (3^2)^2
3. (3^4)^2
4. (3^8)^2
...
식으로 하면 16번으로 하면 65536이 되는데, 여기에 *3을 하여 65537번을 만들수 있습니다.. 결국 16+1번 즉 17번만 계산하면 3^65537을 구할 수가 있습니다.
다시 여기에서 지수의 비트들을 왼쪽에서 오른쪽으로 한번씩 훑으면서 비트마다
1. 해당 비트가 0일 경우 해당 값의 제곱
2. 해당 비트가 1일 경우 해당 비트 자릿수
를 모든 자리를 순회하여 곱하면 해당 값을 얻게 됩니다..
해당 알고리즘을의 식은 다음과 같습니다..
- 해당 내용은 http://cacr.uwaterloo.ca/hac/about/chap14.pdf 14.6.1장 Techniques for general exponentiation에 상세히 나와있습니다.
여기까지 pow()의 연산 방식과 동작 과정을 살짝 맛보기만 보았습니다. 간단하게 재귀식으로 구한 하실 수 있지만, 되도록이면 내장 함수로 되어 있는 함수를 쓰는 걸 추천합니다.
- 번외
pow() 함수를 풀어쓰면
y = b^x === log b ( y ) = x
와 같은 등식이 됩니다. 즉 아래와 같이 치환이 가능하죠.
그리고 위의 binary exponentiation으로 2의 승수로 풀어쓰면
2^(y*log2(x))
이 되는데 해당 수식은 x86의 모든 칩셋에
FLY2X , FLY2XP1으로 명령어 집합이 정의되어 있습니다.
즉 계산이 일반 n번 제곱하여 곱하는것보다 log로 치환하면 CPU 지원으로 엄청나게 빠르다는 이야기!!
진짜 끝!!
참고 자료
- https://yeojin-dev.github.io/blog/exponentiation-by-squaring/
- https://stackoverflow.com/questions/4638473/how-to-powreal-real-in-x86
'app > python' 카테고리의 다른 글
Python integer objects implementation (0) | 2020.03.18 |
---|---|
python hash && cache (0) | 2020.03.11 |
파이썬 백엔드 면접 질문들 (장고+시스템+디비 포함) (0) | 2020.02.21 |
python은 GIL때문에 스레드가 효율적이지 않으면서 왜 multithreading 모듈이 있을까? (0) | 2020.01.13 |
python의 팩토리 패턴과 partial (0) | 2020.01.10 |