어휘 범위로 왜`빠르다 '?


31

dolist매크로 의 소스 코드를 읽는 동안 다음과 같은 주석이 나왔습니다.

;; 이것은 신뢰할 수있는 테스트는 아니지만 두 시맨틱 모두 허용 가능하기 때문에 중요하지 않습니다. 하나는 동적 범위 지정 에서는 약간 더 빠르며 다른 하나는 어휘 범위 지정에서는 약간 더 빠릅니다 (깨끗한 의미론을 가짐) .

이 스 니펫 (명확하게하기 위해 단순화했습니다)을 언급했습니다.

(if lexical-binding
    (let ((temp list))
      (while temp
        (let ((it (car temp)))
          ;; Body goes here
          (setq temp (cdr temp)))))
  (let ((temp list)
        it)
    (while temp
      (setq it (car temp))
      ;; Body goes here
      (setq temp (cdr temp)))))

let루프 내부에서 사용되는 양식 을 보니 놀랐습니다 . 나는 setq동일한 외부 변수 를 반복적으로 사용 하는 것 (위의 두 번째 경우와 마찬가지로)에 비해 느리다고 생각했습니다 .

나는 바로 위의 주석이 그렇지 않으면 (어휘 바인딩이있는) 대안보다 빠르다는 것을 명시 적으로 언급하지 않았다면 그것을 무시했을 것입니다. 그래서 ... 왜 그렇습니까?

  1. 어휘와 동적 바인딩에서 위의 코드 성능이 다른 이유는 무엇입니까?
  2. let어휘를 사용 하여 양식이 더 빠른 이유는 무엇 입니까?

답변:


38

일반적으로 어휘 바인딩과 동적 바인딩

다음 예제를 고려하십시오.

(let ((lexical-binding nil))
  (disassemble
   (byte-compile (lambda ()
                   (let ((foo 10))
                     (message foo))))))

lambda로컬 변수를 사용 하여 단순 을 컴파일하고 즉시 분해 합니다. 함께 lexical-binding장애인, 위와 같이, 바이트 코드의 모습은 다음과 같습니다 :

0       constant  10
1       varbind   foo
2       constant  message
3       varref    foo
4       call      1
5       unbind    1
6       return    

varbindvarref지침을 참고하십시오 . 이러한 명령어 는 힙 메모리전역 바인딩 환경 에서 해당 변수 를 이름별로 바인드하고 조회 합니다 . 이 모든 것은 성능에 악영향을 미칩니다. 문자열 해싱 및 비교 , 전역 데이터 액세스를위한 동기화 및 CPU 캐싱에서 좋지 않은 반복 힙 메모리 액세스가 포함됩니다. 또한 동적 변수 바인딩 은의 끝에서 이전 변수 로 복원 해야하므로 바인딩이있는 각 블록에 대한 추가 조회 가 추가 됩니다.letnletn

만약 당신 바인드 lexical-bindingt위의 예에서 바이트 코드는 다소 다른 모습 :

0       constant  10
1       constant  message
2       stack-ref 1
3       call      1
4       return    

참고 그 varbindvarref완전히 사라졌다. 로컬 변수는 단순히 스택으로 푸시되고 stack-ref명령을 통해 상수 오프셋으로 참조됩니다. 본질적으로 변수는 상수 시간 , 스택 내 메모리 읽기 및 쓰기 로 바인딩되고 읽 히며, 이는 완전히 로컬이므로 동시성 및 CPU 캐싱과 잘 작동하며 문자열을 전혀 포함하지 않습니다.

일반적으로, 로컬 변수의 사전 조회 바인딩 (예로 let, setq등)가 훨씬 적은 메모리 및 실행 복잡성 .

이 특정 예

동적 바인딩을 사용하면 위와 같은 이유로 각각의 성능이 저하됩니다. 더 많은 변수가 동적 변수 바인딩을 허용합니다.

특히, 추가로 let내의 loop본체 바운드 변수 복원 할 필요가 루프의 각 반복 가산, 각 반복에 추가 변수 조회 . 따라서 전체 루프가 끝난 후 반복 변수가 한 번만 재설정되도록 루프 본문을 내보내는 것이 더 빠릅니다 . 그러나 반복 변수가 실제로 필요하기 전에 바인딩되어 있기 때문에 이것은 특히 우아하지 않습니다.

어휘 바인딩을 사용하면 lets가 저렴합니다. 특히 let루프 바디 내부는 루프 바디 let외부 보다 나쁘지 않습니다 (성능면에서) . 따라서 변수를 가능한 한 로컬에 바인딩하고 반복 변수를 루프 본문에 국한시키는 것이 좋습니다.

훨씬 적은 명령으로 컴파일되기 때문에 약간 빠릅니다. 다음에 나오는 병렬 분해를 고려하십시오 (오른쪽에 로컬 렛).

0       varref    list            0       varref    list         
1       constant  nil             1:1     dup                    
2       varbind   it              2       goto-if-nil-else-pop 2 
3       dup                       5       dup                    
4       varbind   temp            6       car                    
5       goto-if-nil-else-pop 2    7       stack-ref 1            
8:1     varref    temp            8       cdr                    
9       car                       9       discardN-preserve-tos 2
10      varset    it              11      goto      1            
11      varref    temp            14:2    return                 
12      cdr       
13      dup       
14      varset    temp
15      goto-if-not-nil 1
18      constant  nil
19:2    unbind    2
20      return    

그러나 차이를 일으키는 원인은 전혀 없습니다.


7

요컨대 동적 바인딩이 매우 느립니다. 어휘 바인딩은 런타임에 매우 빠릅니다. 근본적인 이유는 컴파일시 어휘 바인딩을 해결할 수 있지만 동적 바인딩은 해결할 수 없기 때문입니다.

다음 코드를 고려하십시오.

(let ((x 42))
    (foo)
    (message "%d" x))

를 컴파일 할 때 let컴파일러는 foo(동적 바운드) 변수에 액세스 할 수 있는지 알 수 없으므로에 x대한 바인딩을 작성 x하고 변수의 이름을 유지해야합니다. 어휘 바인딩을 사용하면 컴파일러 는 이름없이 바인딩 스택 의 을 덤프 x하고 올바른 항목에 직접 액세스합니다.

그러나 더 기다려야합니다. 어휘 바인딩을 사용하면 컴파일러는이 특정 바인딩이 x코드에서만 사용 되는지 확인할 수 있습니다 message. 이후 x수정되지 않습니다, 그것은 인라인에 대한 안전 x및 수율

(progn
  (foo)
  (message "%d" 42))

현재 바이트 코드 컴파일러 가이 최적화를 수행한다고 생각하지 않지만 앞으로 그렇게 할 것이라고 확신합니다.

간단히 말해 :

  • 동적 바인딩은 최적화를위한 기회가 거의없는 헤비급 작업입니다.
  • 어휘 바인딩은 간단한 작업입니다.
  • 읽기 전용 값의 어휘 바인딩은 종종 최적화 될 수 있습니다.

3

이 의견은 어휘 바인딩이 동적 바인딩보다 빠르거나 느리다는 것을 암시하지 않습니다. 오히려, 이들 상이한 형태는 어휘 및 동적 결합 하에서 상이한 성능 특성을 갖는다는 것을 시사한다.

그래서 입니다 어휘 범위 빠르게 동적 범위보다 더? 나는이 경우에 큰 차이가 없다고 생각하지만, 나는 모른다-당신은 정말로 그것을 측정해야 할 것이다.


1
varbind어휘 바인딩 아래 컴파일 된 코드 가 없습니다 . 그것이 요점과 목적입니다.
lunaryorn

흠. 나는 어휘 바인딩 아래 컴파일 된 정의를 생성했다고 가정 하고 위의 소스를 포함하는 파일을로 시작하여 ;; -*- lexical-binding: t -*-로드하고 호출했습니다 (byte-compile 'sum1). 그러나 그렇지 않은 것 같습니다.
gsg

잘못된 가정을 기반으로 바이트 코드 주석을 제거했습니다.
gsg

lunaryon의 답변에 따르면이 코드 어휘 바인딩에서 분명히 더 빠릅니다 (물론 마이크로 레벨에서만 가능).
shosti

@gsg이 선언은 표준 파일 변수 일 뿐이며 해당 파일 버퍼 외부에서 호출 된 함수에는 영향을 미치지 않습니다. IOW는 소스 파일을 방문한 다음 byte-compile현재의 해당 버퍼 를 호출하는 경우에만 효과가 있습니다. 즉, 바이트 컴파일러가 수행하는 작업입니다. byte-compile별도로 호출 하면 lexical-binding내 대답에서했던 것처럼 명시 적으로 설정해야합니다 .
lunaryorn
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.