def main():
for i in xrange(10**8):
pass
main()
이 Python 코드는 다음에서 실행됩니다 (참고 : 타이밍은 Linux의 BASH에서 시간 함수로 수행됩니다).
real 0m1.841s
user 0m1.828s
sys 0m0.012s
그러나 for 루프가 함수 내에 배치되지 않으면
for i in xrange(10**8):
pass
훨씬 오랜 시간 동안 실행됩니다.
real 0m4.543s
user 0m4.524s
sys 0m0.012s
왜 이런거야?
답변
전역 변수보다 지역 변수를 저장하는 것이 더 빠른 이유를 물을 수 있습니다 . CPython 구현 세부 사항입니다.
CPython은 인터프리터가 실행하는 바이트 코드로 컴파일됩니다. 함수를 컴파일 할 때, 로컬 변수는 (고정 된 크기 어레이에 저장되어 있지dict
) 및 변수 이름은 인덱스에 할당된다. 함수에 로컬 변수를 동적으로 추가 할 수 없기 때문에 가능합니다. 그런 다음 로컬 변수를 검색하는 것은 말 그대로 목록에 대한 포인터 조회 PyObject
이며 사소한 것에 대한 참조 횟수 증가입니다 .
이것을 전역 조회 ( LOAD_GLOBAL
)와 대조하면 dict
해시 등을 포함한 실제 검색입니다. 덧붙여서 이것이 global i
전역 화되기를 원하는지 지정 해야하는 이유입니다 . 스코프 내의 변수에 할당 한 경우 컴파일러는 STORE_FAST
사용자에게 알리지 않는 한 액세스를 위해 s를 발행 합니다.
그건 그렇고, 글로벌 조회는 여전히 최적화되어 있습니다. 속성 조회 foo.bar
는 정말 느린 것입니다!
다음은 지역 변수 효율성에 대한 작은 그림 입니다.
답변
함수 내에서 바이트 코드는 다음과 같습니다.
2 0 SETUP_LOOP 20 (to 23)
3 LOAD_GLOBAL 0 (xrange)
6 LOAD_CONST 3 (100000000)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 6 (to 22)
16 STORE_FAST 0 (i)
3 19 JUMP_ABSOLUTE 13
>> 22 POP_BLOCK
>> 23 LOAD_CONST 0 (None)
26 RETURN_VALUE
최상위 레벨에서 바이트 코드는 다음과 같습니다.
1 0 SETUP_LOOP 20 (to 23)
3 LOAD_NAME 0 (xrange)
6 LOAD_CONST 3 (100000000)
9 CALL_FUNCTION 1
12 GET_ITER
>> 13 FOR_ITER 6 (to 22)
16 STORE_NAME 1 (i)
2 19 JUMP_ABSOLUTE 13
>> 22 POP_BLOCK
>> 23 LOAD_CONST 2 (None)
26 RETURN_VALUE
차이점은 STORE_FAST
보다 빠릅니다 (!) STORE_NAME
. 함수에서 i
로컬이지만 최상위에서는 전역이기 때문입니다.
바이트 코드를 검사하려면 dis
모듈을 사용하십시오 . 함수를 직접 분해 할 수 있었지만 최상위 코드를 분해하려면 compile
내장 을 사용해야했습니다 .
답변
로컬 / 전역 가변 저장 시간 외에도 opcode 예측 기능으로 기능이 더 빨라집니다.
다른 답변에서 설명 하듯이 함수는 STORE_FAST
루프 에서 opcode를 사용합니다 . 함수 루프의 바이트 코드는 다음과 같습니다.
>> 13 FOR_ITER 6 (to 22) # get next value from iterator
16 STORE_FAST 0 (x) # set local variable
19 JUMP_ABSOLUTE 13 # back to FOR_ITER
일반적으로 프로그램이 실행될 때, 파이썬은 각 opcode를 하나씩 차례로 실행하여 스택을 추적하고 각 opcode가 실행 된 후 스택 프레임에서 다른 검사를 수행합니다. Opcode 예측은 특정 경우 Python이 다음 opcode로 직접 이동할 수 있으므로 이러한 오버 헤드를 피할 수 있음을 의미합니다.
이 경우, 파이썬이 볼 때마다 FOR_ITER
(루프의 상단), STORE_FAST
다음에 수행 할 opcode 를 “예측”합니다 . 그런 다음 파이썬은 다음 opcode를 들여다보고 예측이 정확하면 바로 건너 뜁니다.STORE_FAST
. 이것은 두 개의 opcode를 단일 opcode로 압축하는 효과가 있습니다.
반면, STORE_NAME
opcode는 글로벌 수준의 루프에서 사용됩니다. 파이썬은 *하지 않습니다 * 이 opcode를 볼 때 비슷한 예측을 . 대신, 루프가 실행되는 속도에 명백한 영향을 미치는 평가 루프의 맨 위로 돌아 가야합니다.
이 최적화에 대한 기술적 세부 사항을 제공하기 위해 ceval.c
파일 (Python 가상 머신의 “엔진”) 에서 인용 한 내용은 다음과 같습니다.
일부 opcode는 쌍을 이루는 경향이 있으므로 첫 번째 코드가 실행될 때 두 번째 코드를 예측할 수 있습니다. 예를 들어
GET_ITER
종종 뒤에옵니다FOR_ITER
. 그리고FOR_ITER
종종 뒤에STORE_FAST
나UNPACK_SEQUENCE
.예측을 검증하는 것은 상수에 대한 레지스터 변수의 단일 고속 테스트 비용이 든다. 페어링이 양호하면 프로세서 자체의 내부 분기 예측이 성공할 가능성이 높으므로 다음 opcode로 거의 오버 헤드가 거의 발생하지 않습니다. 성공적인 예측은 예측할 수없는 두 가지 분기 인
HAS_ARG
테스트와 스위치 케이스를 포함하여 평가 루프를 통과하는 여행을 절약합니다 . 프로세서의 내부 분기 예측과 결합PREDICT
하면 두 개의 opcode가 마치 본문이 결합 된 단일 새 opcode 인 것처럼 실행하는 효과가 있습니다.
FOR_ITER
opcode 의 소스 코드에서 정확히 예측 된 위치 를 확인할 수 있습니다 STORE_FAST
.
case FOR_ITER: // the FOR_ITER opcode case
v = TOP();
x = (*v->ob_type->tp_iternext)(v); // x is the next value from iterator
if (x != NULL) {
PUSH(x); // put x on top of the stack
PREDICT(STORE_FAST); // predict STORE_FAST will follow - success!
PREDICT(UNPACK_SEQUENCE); // this and everything below is skipped
continue;
}
// error-checking and more code for when the iterator ends normally
이 PREDICT
함수는 확장되어 if (*next_instr == op) goto PRED_##op
즉, 예측 된 opcode의 시작으로 점프합니다. 이 경우, 우리는 여기서 점프합니다 :
PREDICTED_WITH_ARG(STORE_FAST);
case STORE_FAST:
v = POP(); // pop x back off the stack
SETLOCAL(oparg, v); // set it as the new local variable
goto fast_next_opcode;
이제 로컬 변수가 설정되고 다음 opcode가 실행됩니다. 파이썬은 반복이 끝날 때까지 계속 반복하여 매번 성공적인 예측을합니다.
파이썬 위키 페이지는 CPython과의 가상 머신이 작동하는 방법에 대한 자세한 정보가 있습니다.