python - 파이썬 모듈 만들기




함수에서 파이썬 코드가 더 빨리 실행되는 이유는 무엇입니까? (2)

def main():
    for i in xrange(10**8):
        pass
main()

파이썬에서이 코드 조각은 다음에서 실행됩니다. (참고 : 타이밍은 Linux의 BASH에서 time 함수로 수행됩니다.)

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

왜 이런거야?


로컬 / 글로벌 변수 저장 시간 외에, 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

일반적으로 프로그램이 실행되면 Python은 각 opcode를 하나씩 실행하고 스택을 추적하며 각 opcode가 실행 된 후 스택 프레임에서 다른 검사를 수행합니다. Opcode 예측은 어떤 경우에는 파이썬이 다음 opcode로 바로 이동할 수 있음을 의미하므로 일부 오버 헤드를 피할 수 있습니다.

이 경우, 파이썬은 FOR_ITER (루프의 최상위)를 볼 때마다 STORE_FAST 가 실행 STORE_FAST 다음 opcode임을 "예측"합니다. 그런 다음 파이썬은 다음 opcode를 들여다보고 예측이 정확하다면 곧바로 STORE_FAST 점프합니다. 이것은 두 opcode를 하나의 opcode로 집어 넣는 효과가 있습니다.

반면 STORE_NAME opcode는 전역 수준의 루프에서 사용됩니다. 파이썬은이 opcode를 볼 때 비슷한 예측을 하지 않습니다 . 대신, 루프가 실행되는 속도에 대한 명백한 함의가있는 평가 루프의 맨 위로 되돌아 가야합니다.

이 최적화에 대한 기술적 세부 사항을 좀 더 알려면 ceval.c 파일 (파이썬 가상 머신의 "엔진")의 인용구를 사용하십시오.

일부 opcode는 쌍으로되어 경향이 있으므로 첫 번째 코드가 실행될 때 두 번째 코드를 예측할 수 있습니다. 예를 들어, GET_ITER 뒤에는 종종 GET_ITERFOR_ITER . FOR_ITER 뒤에는 종종 STORE_FAST 또는 UNPACK_SEQUENCE .

예측에 대한 검증은 상수에 대한 레지스터 변수의 단일 고속 테스트에 비용이 듭니다. 페어링이 양호한 경우 프로세서 자체의 내부 분기 예측은 성공 확률이 높으므로 오버 코드가 거의 없기 때문에 다음 opcode로 거의 전환되지 않습니다. 성공적인 예측은 두 개의 예기치 않은 분기, HAS_ARG 테스트 및 스위치 케이스를 포함하여 평가 루프를 통과하는 여행을 저장합니다. 프로세서의 내부 분기 예측과 함께 성공적인 PREDICT 는 두 op 코드가 마치 시체가 결합 된 단일 opcode 인 것처럼 실행됩니다.

소스 코드에서 FOR_ITER 연산 코드를 정확히 볼 수 있습니다. 여기서 FOR_ITER 에 대한 예측은 다음과 같습니다.

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 확장됩니다. 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가 실행됩니다. 파이썬은 끝까지 도달 할 때까지 iterable을 계속 진행하여 매번 성공적인 예측을합니다.

파이썬 위키 페이지 에는 CPython의 가상 머신이 어떻게 작동하는지에 대한 더 많은 정보가 있습니다.


전역 변수보다 지역 변수를 저장하는 것이 더 빠른 이유를 묻습니다. 이것은 CPython 구현 세부 사항입니다.

CPython은 인터프리터가 실행하는 바이트 코드로 컴파일된다는 것을 기억하십시오. 함수가 컴파일 될 때 지역 변수는 고정 크기 배열 ( dict 아닌 )에 저장되고 변수 이름은 인덱스에 할당됩니다. 이는 함수에 지역 변수를 동적으로 추가 할 수 없기 때문에 가능합니다. 그런 다음 로컬 변수를 검색하는 것은 말 그대로 목록에 대한 포인터 검색이며 PyObject 에 대한 참조 PyObject 는 사소한 것입니다.

이것을 글로벌 조회 ( LOAD_GLOBAL )와 LOAD_GLOBAL . 이것은 해시를 포함하는 실제 dict 검색입니다. 덧붙여 말하자면 global i 를 지정하려면 global i 를 지정해야합니다. 범위 안의 변수에 할당 한 경우 컴파일러는 사용자가 지정하지 않는 한 해당 액세스에 STORE_FAST 를 실행합니다.

그건 그렇고, 글로벌 조회는 여전히 매우 최적화되어 있습니다. 속성 검색 foo.bar정말 느린 것들입니다!

다음은 지역 변수 효율성에 대한 작은 illustration 입니다.





cpython