๐Ÿ List Comprehension์ด ๋น ๋ฅธ ์ด์œ ๋ฅผ ์ฐพ์•„๋ณด์ž

Python์„ ์–ด๋Š์ •๋„ ์“ฐ๋Š” ์‚ฌ๋žŒ์ด๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๋“ฃ๋Š” โ€œList Append๋ฅผ ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค List Comprehension์„ ์จ์„œ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ๋” ๋น ๋ฅด๊ณ  ๊ฐ„๊ฒฐํ•˜๋‹ค.โ€๋ผ๋Š” ๋ง. ํ•˜์ง€๋งŒ ์‹ค์ œ ๋‚ด๋ถ€ ๋™์ž‘๊ณผ ๋”๋ถˆ์–ด ์„ค๋ช…ํ•˜๋Š” ์‚ฌ๋žŒ์€ ๋“œ๋ฌผ๋‹ค. ์‹ค์ œ ๊ตฌํ˜„์ด ์–ด๋–ป๊ฒŒ ๋˜์–ด ์žˆ๊ธธ๋ž˜ ๊ทธ๋ ‡๊ฒŒ๋“ค ๋งํ•˜๋Š” ๊ฒƒ์ผ๊นŒ?

Python 3.7.7 ๊ธฐ์ค€์œผ๋กœ ์ž‘์„ฑ๋˜์—ˆ์œผ๋ฉฐ, ๋ฒ„์ „ ๋ณ„๋กœ ์‹ค์ œ ๋™์ž‘์€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Œ์„ ์•Œ๋ ค๋“œ๋ฆฝ๋‹ˆ๋‹ค.

์†๋„ ๋น„๊ต

$ python3.7 -m timeit "result = []
dquote> for i in range(10000000):
dquote>     result.append(i)"
1 loop, best of 5: 641 msec per loop
$ python3.7 -m timeit "result = [i for i in range(10000000)]"
1 loop, best of 5: 388 msec per loop

์šฐ์„  ์•„๋ž˜์˜ ๋ฌธ๋ฒ•์ด List Comp ๋ฐฉ์‹์ด๊ณ  ์œ„ ๋ฐฉ์‹์ด for loop๋ฅผ ๋Œ๋ฉฐ appendํ•ด๊ฐ€๋Š” ๋ฐฉ์‹์ด๋‹ค. ์ด ๋‘๊ฐ€์ง€ ๋ฐฉ์‹์˜ ์‹คํ–‰์‹œ๊ฐ„์„ timeit์œผ๋กœ ๋น„๊ตํ•ด๋ณด๋‹ˆ ๊ฝค ๋งŽ์€ ์‹œ๊ฐ„์ด ์ฐจ์ด๊ฐ€ ๋‚œ๋‹ค. ์ด ๋‘๊ฐ€์ง€ ๋ฐฉ์‹์˜ ์ฐจ์ด๋Š” ๋ฌด์—‡์ผ๊นŒ?

Byte code ๋น„๊ต

Python 3.7.7 (default, Jun 20 2020, 16:27:13)
[Clang 11.0.3 (clang-1103.0.32.62)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import dis
>>> def append():
...     result = []
...     for i in range(10000):
...             result.append(i)
...     return result
...
>>> dis.dis(append)
  2           0 BUILD_LIST               0
              2 STORE_FAST               0 (result)

  3           4 SETUP_LOOP              26 (to 32)
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               1 (10000)
             10 CALL_FUNCTION            1
             12 GET_ITER
        >>   14 FOR_ITER                14 (to 30)
             16 STORE_FAST               1 (i)

  4          18 LOAD_FAST                0 (result)
             20 LOAD_METHOD              1 (append)
             22 LOAD_FAST                1 (i)
             24 CALL_METHOD              1
             26 POP_TOP
             28 JUMP_ABSOLUTE           14
        >>   30 POP_BLOCK

  5     >>   32 LOAD_FAST                0 (result)
             34 RETURN_VALUE
>>> timeit.timeit("append()", setup="from __main__ import append", number=10000)
6.290953219999892

append ๋ฐฉ์‹์„ dis.dis๋กœ ๋ฐ”์ดํŠธ ์ฝ”๋“œ๋ฅผ ์‚ดํŽด๋ณด์ž. BUILD_LIST ํ›„ SETUP_LOOP ํ•˜๊ณ , 14 ~ 28๊นŒ์ง€ ๋ฃจํ”„๋ฅผ ๋ˆ๋‹ค.

>>> def list_comp():
...     return [i for i in range(10000)]
...
>>> dis.dis(list_comp)
  2           0 LOAD_CONST               1 (<code object <listcomp> at 0x10df3d660, file "<stdin>", line 2>)
              2 LOAD_CONST               2 ('list_comp.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (10000)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x10df3d660, file "<stdin>", line 2>:
  2           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (i)
              8 LOAD_FAST                1 (i)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE
>>> timeit.timeit("list_comp()", setup="from __main__ import list_comp", number=10000)
3.2182092880000255

List Comprehension์ด ๊ต‰์žฅํžˆ ํŠน์ดํ–ˆ๋˜ ๊ฒƒ์ด listcomp๋กœ MAKE_FUNCTION์„ ํ•œ ํ›„ ํ•ด๋‹น ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. LOAD_GLOBAL, LOAD_CONST, CALL_FUNCTION, GET_ITER๋กœ range ํ•จ์ˆ˜์˜ ๋ฐ˜ํ™˜๊ฐ’์„ ๋ฐ›์•„์˜จ ๋‹ค์Œ์— CALL_FUNCTION์œผ๋กœ List Comprehension์„ ํ˜ธ์ถœํ•œ๋‹ค.

์ƒ์„ธ ๋ถ„์„

์šฐ์„  ๋‹ค๋ฅด๋‹ค๊ณ  ์˜์‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์€ List Append ๋ถ€๋ถ„๊ณผ Function Call ๋ถ€๋ถ„์ด๋‹ค.

List Append๋ฅผ ๋ฐ”์ดํŠธ ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ตœ์ ํ™”๊ฐ€ ๋˜์–ด์žˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ด๋ณผ ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๊ณ , Function Call ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๊ต‰์žฅํžˆ ํฌ๋‹ค๊ณ  ๊ฐ€์ •ํ•  ๊ฒฝ์šฐ์— Iteration์•ˆ์— CALL_METHOD๊ฐ€ ์žˆ๋Š” Append ๋ฐฉ์‹์ด ๋œ ์ตœ์ ํ™”๊ฐ€ ๋œ ์ฝ”๋“œ๋ผ๊ณ  ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ ๊ฐ™๋‹ค.

LIST_APPEND Byte Code

๊ทธ๋Ÿผ ์‹ค์ œ ๋ฐ”์ดํŠธ ์ฝ”๋“œ ๊ตฌํ˜„์„ ์‚ดํŽด๋ณด์ž. LIST_APPEND์˜ ๋ฐ”์ดํŠธ ์ฝ”๋“œ ํ•ด์„ ๋ถ€๋ถ„์€ https://github.com/python/cpython/blob/3.7/Python/ceval.c#L1382์— ์žˆ๊ณ , ํ•ด๋‹น ์ผ€์ด์Šค๋Š” PyList_Append ํ•จ์ˆ˜(https://github.com/python/cpython/blob/3.7/Objects/listobject.c#L301)๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

ํ•˜์ง€๋งŒ ํ•ด๋‹นํ•จ์ˆ˜์—์„œ ํ˜ธ์ถœํ•˜๋Š” app1์€ ์‹ค์ œ๋กœ Python์˜ list ๊ตฌํ˜„์ฒด์˜ append ๋ฉ”์†Œ๋“œ(https://github.com/python/cpython/blob/3.7/Objects/listobject.c#L802)์—์„œ ์‚ฌ์šฉํ•˜๋Š” ํ•จ์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ™์€ ๊ตฌํ˜„์ฒด๋ผ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ์ •๋ง dis๋ชจ๋“ˆ์˜ LIST_APPEND ์„ค๋ช…์ฒ˜๋Ÿผ ๊ทธ๋ƒฅ List Comprehension์šฉ์œผ๋กœ ๊ตฌํ˜„๋œ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค. ๋‹น์—ฐํžˆ memory resizeํ•˜๋Š” ์ „๋žต๋„ ๊ฐ™๋‹ค.

CALL_METHOD Byte Code

Python์˜ CALL_METHOD ๊ตฌํ˜„์„ ์ฐพ์•„๋ณด๋ฉด https://github.com/python/cpython/blob/3.7/Python/ceval.c#L3071์— ์žˆ๊ณ , ๋ถ€๋ฅด๋Š” ํ•จ์ˆ˜๋ฅผ ํƒ€๊ณ ํƒ€๊ณ  ๋“ค์–ด๊ฐ€๋ฉด์„œ ์ฝ๋‹ค๋ณด๋ฉด CallStack์„ ์ €์žฅํ•˜๋ฉด์„œ ํ•จ์ˆ˜ Input, Output ํ™•์ธํ•˜๊ณ  ์‹คํ–‰ํ•˜๋Š” ์ฝ”๋“œ์ธ ๊ฒƒ์œผ๋กœ ๋ณด์ธ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ํ™•์ธํ•˜๋ฉด ๊ต‰์žฅํžˆ ์˜ค๋ž˜ ๊ฑธ๋ฆด ๊ฒƒ ๊ฐ™์•„์„œ ๊ฐ„๋‹จํ•˜๊ฒŒ timeit์œผ๋กœ ํ™•์ธ์„ ํ•ด๋ณด๋‹ˆ

$ python3.7 -m timeit -s "def empty(): pass" "for i in range(10000000): empty()"
1 loop, best of 5: 555 msec per loop

์ฐพ์•˜๋‹ค!

๋” ์ฐพ์•„๋ณด์ž

๊ทธ๋Ÿผ ํ•จ์ˆ˜ ์ฝœ ์•ˆํ•˜๊ณ  ๋ฃจํ”„๋งŒ ๋„๋Š” ๊ฒƒ์ด ์–ผ๋งˆ๋‚˜ ๋Š๋ฆด๊นŒ?

$ python3.7 -m timeit "for i in range(10000000): pass"
2 loops, best of 5: 164 msec per loop

๊ทธ๋Ÿผ (ํ•จ์ˆ˜ ์ฝœ + ๋ฃจํ”„) - (๋ฃจํ”„) => ํ•จ์ˆ˜๋งŒ ์ฝœํ•˜๋Š” ์‹œ๊ฐ„ .. ํ•˜๋ฉด ๊ทธ๋ƒฅ ํ•จ์ˆ˜ ์ฝœ์ด ๊ฒ๋‚˜ ๋Š๋ฆฌ๋‹ค.

์ •๋ฆฌ

์—ญ์‹œ๋‚˜ ์กฐ๊ธˆ ๊ธ€์ด ์ •๋ฆฌ๊ฐ€ ๋งŽ์ด ์•ˆ๋˜์—ˆ์ง€๋งŒ, list comprehension์ด ๋น ๋ฅธ ์ด์œ ๋Š” Loop + Append ๋ฐฉ์‹์— ๋น„ํ•ด Function Call ํšŸ์ˆ˜๋ฅผ ๊ต‰์žฅํžˆ ๋งŽ์ด ์ค„์˜€๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

June 21, 2020 ์— ์ž‘์„ฑ
Tags: python