custom_min() vs 내장 함수 min(), 그리고 그 차이

2023. 1. 2. 17:27기술 공부/Python

이번엔 조금 재밌는 것을 다뤄볼 것이다.

바로 내가 만든 custom_min()과
Python이 제공하는 내장 함수 min()의 속도 대결을 붙여볼 것이다.

결론부터 말하자면, Python만을 이용해 만든 함수는
Python이 제공하는 내장 함수를 절-대 이길 수 없다.

바로 도전적으로 코드를 만들어보자.


우선 최솟값을 골라낼 리스트를 먼저 소개하겠다.

import random


_list = random.sample(range(-1000000000, 1000000001), k=100000000)

-10억부터 10억까지의 범위에서 랜덤 하게 1억 개의 숫자를 뽑아서 만든 리스트이다.

 

 

 

그리고 오늘의 도전자. custom_min().
Python이 제공하는 min() 함수를 직접 만들어 보았다.

가장 원초적인 모습으로 만들었다. 직접 따로 만들어보아도 된다.
어차피 우승은 내장 함수;

def custom_min(l: list):
    minimum = l[0]
    for n in l[1:]:
        if minimum > n:
            minimum = n

    return minimum


print(custom_min(_list))

 

 

 

다음으로 챔피언, 내장 함수 min().

print(min(_list))

 

 

 

시간 측정을 위해 time으로 둘둘 말아주고, 바로 돌려보자.
(시간은 컴퓨터의 잉여 자원에 따라 달라질 수 있다.
내장 함수 min()의 속도가 더 빠르다는 사실만 참고하자.)
( + 직접 돌려보는 것은 그다지 추천하지 않는다.)

import time
import random


def custom_min(l: list):
    minimum = l[0]
    for n in l[1:]:
        if minimum > n:
            minimum = n

    return minimum


_list = random.sample(range(-1000000000, 1000000001), k=100000000)

print('custom_min()')
st = time.time()
print(custom_min(_list))
print(f'{time.time() - st:0.10f} sec')

print()
print('내장 함수 min()')
st = time.time()
print(min(_list))
print(f'{time.time() - st:0.10f} sec')
출력 결과 : 

custom_min()
-999999999
6.8395049572 sec

내장 함수 min()
-999999999
2.3972830772 sec

약 4.5초나 차이난다. 왜 어째서일까.


그 이유는 Python의 내장 함수 코드를 보면 알 수 있다.

https://github.com/python/cpython/blob/main/Python/bltinmodule.c

 

GitHub - python/cpython: The Python programming language

The Python programming language. Contribute to python/cpython development by creating an account on GitHub.

github.com

시작부터 낌새가 이상하다. '. c'로 끝나는 파일이라니.
그렇다. 내장 함수는 Python으로 만들어져 있지 않다.
기본적으로 c 언어로 만들어져 있다.

 

 

 

이 Git File의 1837번째 줄에 있는 코드를 보자.

static PyObject *
builtin_min(PyObject *self, PyObject *args, PyObject *kwds)
{
    return min_max(args, kwds, Py_LT);
}

builtin_min은 다시 min_max를 호출한다.

그리고 1727번째 줄의 min_max는 대충 이렇게 생겼다.

static PyObject *
min_max(PyObject *args, PyObject *kwds, int op)
{
    PyObject *v, *it, *item, *val, *maxitem, *maxval, *keyfunc=NULL;
    PyObject *emptytuple, *defaultval = NULL;
    static char *kwlist[] = {"key", "default", NULL};
    const char *name = op == Py_LT ? "min" : "max";
    const int positional = PyTuple_Size(args) > 1;
    int ret;

    if (positional) {
        v = args;
    }
    else if (!PyArg_UnpackTuple(args, name, 1, 1, &v)) {
        if (PyExceptionClass_Check(PyExc_TypeError)) {
            PyErr_Format(PyExc_TypeError, "%s expected at least 1 argument, got 0", name);
        }
        return NULL;
    }

    emptytuple = PyTuple_New(0);
    if (emptytuple == NULL)
        return NULL;
    ret = PyArg_ParseTupleAndKeywords(emptytuple, kwds,
                                      (op == Py_LT) ? "|$OO:min" : "|$OO:max",
                                      kwlist, &keyfunc, &defaultval);
    Py_DECREF(emptytuple);
    if (!ret)
        return NULL;

    if (positional && defaultval != NULL) {
        PyErr_Format(PyExc_TypeError,
                        "Cannot specify a default for %s() with multiple "
                        "positional arguments", name);
        return NULL;
    }

    it = PyObject_GetIter(v);
    if (it == NULL) {
        return NULL;
    }

    if (keyfunc == Py_None) {
        keyfunc = NULL;
    }

    maxitem = NULL; /* the result */
    maxval = NULL;  /* the value associated with the result */
    while (( item = PyIter_Next(it) )) {
        /* get the value from the key function */
        if (keyfunc != NULL) {
            val = PyObject_CallOneArg(keyfunc, item);
            if (val == NULL)
                goto Fail_it_item;
        }
        /* no key function; the value is the item */
        else {
            val = Py_NewRef(item);
        }

        /* maximum value and item are unset; set them */
        if (maxval == NULL) {
            maxitem = item;
            maxval = val;
        }
        /* maximum value and item are set; update them as necessary */
        else {
            int cmp = PyObject_RichCompareBool(val, maxval, op);
            if (cmp < 0)
                goto Fail_it_item_and_val;
            else if (cmp > 0) {
                Py_DECREF(maxval);
                Py_DECREF(maxitem);
                maxval = val;
                maxitem = item;
            }
            else {
                Py_DECREF(item);
                Py_DECREF(val);
            }
        }
    }
    if (PyErr_Occurred())
        goto Fail_it;
    if (maxval == NULL) {
        assert(maxitem == NULL);
        if (defaultval != NULL) {
            maxitem = Py_NewRef(defaultval);
        } else {
            PyErr_Format(PyExc_ValueError,
                         "%s() arg is an empty sequence", name);
        }
    }
    else
        Py_DECREF(maxval);
    Py_DECREF(it);
    return maxitem;

Fail_it_item_and_val:
    Py_DECREF(val);
Fail_it_item:
    Py_DECREF(item);
Fail_it:
    Py_XDECREF(maxval);
    Py_XDECREF(maxitem);
    Py_DECREF(it);
    return NULL;
}

c 언어이니 몰라도 사는 데에는 아무 지장 없다. 알면 좋을 뿐.

더 킹 받는 건, Python으로 만들어지지 않았기 때문에,
Python의 DocString도 존재하지 않는다.

 

 

 

그래서...

PyDoc_STRVAR(min_doc,
"min(iterable, *[, default=obj, key=func]) -> value\n\
min(arg1, arg2, *args, *[, key=func]) -> value\n\
\n\
With a single iterable argument, return its smallest item. The\n\
default keyword-only argument specifies an object to return if\n\
the provided iterable is empty.\n\
With two or more arguments, return the smallest argument.");

'c 언어 파일의' 1843번째 줄에 이렇게 적혀있다...

 

 

 

Python의 help 클래스를 사용해 확인해 보면,

help(min)
출력 결과 : 

Help on built-in function min in module builtins:

min(...)
    min(iterable, *[, default=obj, key=func]) -> value
    min(arg1, arg2, *args, *[, key=func]) -> value
    
    With a single iterable argument, return its smallest item. The
    default keyword-only argument specifies an object to return if
    the provided iterable is empty.
    With two or more arguments, return the smallest argument.

토씨 하나 안 틀리고 그대로 나온다.

 

 

 

그렇다. 내장 함수 min()을 속도로 절-대 이길 수 없는 이유는.
c 언어로 만들어져 있기 때문이다.

그렇기 때문에 min()을 포함한 Python이 제공하는 (또는 cpython으로 구현된)
내장 함수를 사용하는 것이,

메모리 측면에서도 효율적으로 좋고, 속도 측면에서도 훨씬 빠르다.


그럼 왜 때문에 C 언어가 더 빠르고,
왜 때문에 Python을 쓰는 것일까.

우선 C 언어가 Python 보다 빠른 이유를 알아보자.

 

 

 

변수 a와 b에 1을 저장하고,
둘을 더해 변수 c에 저장하는
C 언어 코드가 있다.

int a = 1;
int b = 1;
int c = a + b;

이 코드는 아마도 이렇게 작동할 것이다.

1. inteager 형식의 1을 2~4 Bytes 크기의 메모리에 할당한다.
2. a에 주소값을 저장한다.

3. inteager 형식의 1을 2~4 Bytes 크기의 메모리에 할당하고
4. b에 주소값을 저장한다.

5. a와 b의 값을 더한다.

6. inteager 형식의 2를 2~4 Bytes 크기의 메모리에 할당한다.
7. c에 주소값을 저장한다.

 

 

 

똑같은 Python 언어 코드가 있다.

a = 1;
b = 1;
c = a + b;

이 코드는 아마도 이렇게 작동할 것이다.

1. 메모리에 PyObject를 생성한다.
2. 1을 생성된 PyObject에 할당한다.
3. 저장된 값의 형식을 검사한다.
4. 확인된 형식을 PyObject_HEAD에 저장한다.
5. PyObject가 저장된 주소 값을 a에 저장한다.

6. 메모리에 PyObject를 생성한다.
7. 1을 생성된 PyObject에 할당한다.
8. 저장된 값의 형식을 검사한다.
9. 확인된 형식을 PyObject_HEAD에 저장한다.
10. PyObject가 저장된 주소 값을 b에 저장한다.

11. a의 PyObject에 저장된 PyObject_HEAD의 형식이 정수인 것을 확인한다.
12. b의 PyObject에 저장된 PyObject_HEAD의 형식이 정수인 것을 확인한다.
13. a와 b의 값을 더한다.

14. 메모리에 PyObject를 생성한다.
15. 2를 생성된 PyObject에 할당한다.
16. 저장된 값의 형식을 검사한다.
17. 확인된 형식을 PyObject_HEAD에 저장한다.
18. PyObject가 저장된 주소 값을 c에 저장한다.

 

 

 

내용을 안 보고, 양만 보더라도 벌써 Python이 하는 일이 많다.
하는 일이 더 많다는 것은 시간이 더 오래 걸린다는 이야기와 같다.

그리고 하는 일이 더 많아, 시간이 더 오래 걸린다는 것이
바로 Python이 C 언어보다 느린 주된 이유이다.

 

 

 

그러면 Python은 왜 하는 일이 더 많아질 수밖에 없는 것일까.
그 해답은 Python의 '동적 타이핑'에 있다.

우리는 Python에게 무엇이든 저장하라고 말할 수 있다.

a = print
b = 1
c = 'c'
d = int()
e = 2 ** 65
f = 1.9128
g = list
h = []
i = [[]]
j = "Hi!"

어떻게? PyObject라는 상자를 사용해서.
그렇기 때문에 우리는 더 편하게 Python에게 일을 시킬 수 있다.
Python님이 그만큼 더 일한다는 말이다.

Python이 안 하면 우리가 그만큼 상세하게 하나씩 타이핑해줘야 한다는 이야기이다.

 

 

 

빠르게 유튜브 영상을 하나 봐보자.
Python, C/C++, Assembly 언어의 프로그래밍 속도와 실행 속도를 비교한 영상이다.

Python vs C/C++ vs Assembly side-by-side comparison

Python이 코드 작성 속도가 가장 빠른 반면, 실행 속도는 꼴찌로 나오게 된다.
빠르게 개발한 후 테스트 실행시켜 놓고 커피와 웹툰을 즐기려면 Python이 최고란 말이다.

 

 

 

추가로, Python은 아까 확인해 보았듯,
C 언어로도 구현이 되어 있고, C 언어가 실행되기도 한다.

이 말을 조금 응용해 보자면, 내장(구현)되어 있는 CPython이 아니어도,
누군가 만들어둔 C 언어 라이브러리를 가져와 사용할 수도 있다는 이야기이다.

즉, Python을 통해 우리는 빠릿빠릿한 C 언어도
누군가느님이 Python에서도 사용할 수 있게 만들어만 주신다면
손쉽게 사용이 가능하다는 이야기이다.

그리고 이 것이 과학 분야에서 Python이 가장 많이 사용되는 큰 이유라고 한다.
이 것이 사실인지는 모르겠지만, 틀린 말 같지도 않다.
(대부분의 (과학 분야를 포함한) 기계의 소프트웨어가 C 언어로 되어 있는 것 때문이지 않을까 추측해 본다. 아님 말고...)

 

 

 

결론을 지어보자.

C 언어로 구현된 Python 내장 함수는 적극적으로 애용하자.

Python이 메모리를 비효율적으로 많이 사용해도 미워하지 말자.

Python의 실행 속도는 느리지만, 프로그래밍 속도는 빠르다.
워라밸에 최적화되어 있다. ㅋ.ㅋ

다 각자의 존재 이유가 있고, 매력이 있다.

정도가 되겠다.


출처

Python Git - cpython/bltinmodule.c at main
https://github.com/python/cpython/blob/main/Python/bltinmodule.c

 

GitHub - python/cpython: The Python programming language

The Python programming language. Contribute to python/cpython development by creating an account on GitHub.

github.com

 

 

Why Python is Slow: Looking Under the Hood
http://jakevdp.github.io/blog/2014/05/09/why-python-is-slow/

 

Why Python is Slow: Looking Under the Hood | Pythonic Perambulations

So Why Use Python?¶ Given this inherent inefficiency, why would we even think about using Python? Well, it comes down to this: Dynamic typing makes Python easier to use than C. It's extremely flexible and forgiving, this flexibility leads to efficient use

jakevdp.github.io

 

Python vs C/C++ vs Assembly side-by-side comparison
https://www.youtube.com/watch?v=3PcIJKd1PKU

 

'기술 공부 > Python' 카테고리의 다른 글

Decorators  (0) 2023.02.01
Python은 어떻게 작동할까? (2)  (2) 2022.12.30
Python은 어떻게 작동할까? (1)  (1) 2022.12.25
Python - class, function, method  (1) 2022.12.23