TL; DR 느린 루프는 Array 'out-of-bounds'에 액세스하기 때문에 엔진이 최적화를하지 않거나 전혀 최적화하지 않고 함수를 다시 컴파일하거나 이러한 최적화로 함수를 컴파일하지 않아야합니다. (JIT-) 컴파일러가 첫 번째 컴파일 'version'전에이 조건을 감지 / 의심 한 경우)를 아래에서 읽으십시오.
누군가 단지
가 이 (완전히 깜짝 놀라게 아무도 이미 않았다) 말 :
영업의 조각은 프로그래밍 책 개요 의도 초보자의 사실상의 예가 될 것이다 때 시간이있을 사용 / 자바 스크립트 '배열'인덱스 시작임을 강조 1이 아닌 0에서 시작하여 일반적인 '초보자 실수'의 예로 사용됩니다 ( '프로그래밍 오류'구절을 피하는 방법을 좋아하지 않습니다
;)
) :
범위를 벗어난 배열 액세스 .
예 1 : 0 기반 인덱싱 (항상 ES262)을 사용하는 5 개 요소 중 5 개 요소
의 Dense Array
(인접한 (인덱스 사이에 간격이 없음을 의미하며 실제로 각 인덱스의 요소를 의미)).
var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
// indexes are: 0 , 1 , 2 , 3 , 4 // there is NO index number 5
따라서 우리는 실제로 <
vs <=
(또는 '한 번의 추가 반복') 간의 성능 차이에 대해 이야기하는 것이 아니라
'올바른 스 니펫 (b)이 잘못된 스 니펫 (a)보다 빠르게 실행되는 이유'는 무엇입니까?
대답은 2 배입니다 (ES262 언어 구현 자의 관점에서는 두 가지 모두 최적화의 형태 임).
- 데이터 표현 : 배열을 메모리에 내부적으로 나타내거나 저장하는 방법 (객체, 해시 맵, '실제'숫자 배열 등)
- Functional Machine-code : 이러한 '어레이'에 액세스 / 처리 (읽기 / 수정)하는 코드를 컴파일하는 방법
항목 1은 받아 들여진 대답에 의해 충분히 (그리고 정확하게 IMHO) 설명되어 있지만, 항목 2 : 컴파일 에 2 단어 ( '코드') 만 사용합니다 .
보다 정확하게 : JIT- 컴파일 및 더욱 중요한 JIT- RE- 컴파일!
언어 사양은 기본적으로 일련의 알고리즘에 대한 설명입니다 ( '정의 된 최종 결과를 달성하기 위해 수행하는 단계'). 그것은 언어를 묘사하는 매우 아름다운 방법입니다. 그리고 엔진이 특정 결과를 구현 자에게 공개하기 위해 사용하는 실제 방법을 남겨두고 정의 된 결과를 생성하는보다 효율적인 방법을 제시 할 수있는 충분한 기회를 제공합니다. 사양 준수 엔진은 정의 된 입력에 대해 사양 준수 결과를 제공해야합니다.
이제 자바 스크립트 코드 / 라이브러리 / 사용률이 증가하고 '실제'컴파일러가 사용하는 리소스 (시간 / 메모리 / 기타)의 양을 기억하면서 사용자가 웹 페이지를 방문하는 시간을 오래 기다릴 수 없다는 점은 분명합니다 (필요합니다). 많은 리소스를 사용할 수 있도록).
다음과 같은 간단한 기능을 상상해보십시오.
function sum(arr){
var r=0, i=0;
for(;i<arr.length;) r+=arr[i++];
return r;
}
완벽하게 명확합니까? 추가 설명이 필요하지 않습니까? 반환 유형은 Number
맞습니까?
음 .. 아니오, 아니오 & 아니오 ... 명명 된 함수 매개 변수에 전달할 인수에 따라 다릅니다 arr
.
sum('abcde'); // String('0abcde')
sum([1,2,3]); // Number(6)
sum([1,,3]); // Number(NaN)
sum(['1',,3]); // String('01undefined3')
sum([1,,'3']); // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]); // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]); // Number(8)
문제를 보시겠습니까? 그렇다면 이것이 가능한 큰 순열을 간신히 버리고 있다고 생각하십시오 ... 우리는 완료 될 때까지 어떤 유형의 RETURN 함수조차 알지 못합니다 ...
이제 동일한 함수 코드 가 문자 그대로 (소스 코드에서) 완전히 설명되고 동적으로 프로그램 내에서 생성 된 '배열'과 같이 다른 유형이나 입력의 변형에 실제로 사용되고 있다고 상상해보십시오 .
따라서 함수 sum
JUST ONCE 를 컴파일 하는 경우 모든 유형의 입력에 대해 항상 스펙 정의 결과를 리턴하는 유일한 방법은 스펙 스펙이 지정된 모든 메인 AND 서브 단계 만 수행하면 스펙 준수 결과를 보장 할 수 있습니다. (이름없는 y2k 브라우저와 같은). (가정이 없기 때문에) 최적화가없고 느린 느린 해석 스크립트 언어가 남아 있습니다.
JIT 컴파일 (JIT In Just Time)은 현재 널리 사용되는 솔루션입니다.
따라서 함수의 기능, 반환 및 수락에 대한 가정을 사용하여 함수를 컴파일하기 시작합니다.
함수가 사양에 맞지 않는 결과를 반환하기 시작하는지 (예 : 예기치 않은 입력을 받기 때문에) 감지하기 위해 가능한 한 간단한 검사를 수행합니다. 그런 다음 이전에 컴파일 된 결과를 버리고 더 정교한 것으로 다시 컴파일하고 이미 가지고있는 부분 결과로 무엇을해야하는지 (신뢰할 수 있거나 확실하게 다시 계산할 수 있는지) 결정하고 함수를 프로그램에 다시 연결하고 다시 시도하십시오. 궁극적으로 스펙에서와 같이 단계적으로 스크립트 해석으로 되돌아갑니다.
이 모든 시간이 걸립니다!
모든 브라우저는 엔진에서 작동하며, 모든 하위 버전마다 개선 및 회귀를 볼 수 있습니다. 문자열은 역사상 어느 시점에서 실제로 불변의 문자열이었습니다 (따라서 array.join은 문자열 연결보다 빠릅니다). 이제 우리는 로프 (또는 유사한)를 사용하여 문제를 완화시킵니다. 둘 다 사양에 맞는 결과를 반환하며 이것이 중요합니다!
짧은 이야기 : 자바 스크립트의 언어 의미가 종종 우리를 다시 얻었 기 때문에 (OP의 예 에서이 조용한 버그와 같이) '멍청한'실수로 인해 컴파일러가 빠른 기계 코드를 뱉을 확률이 높아지는 것은 아닙니다. 그것은 우리가 '보통'올바른 지시를 썼다고 가정한다 : 우리가 '사용자'(프로그래밍 언어)의 현재 진언은 : 컴파일러를 도와주고, 우리가 원하는 것을 설명하고, 일반적인 관용구를 선호한다 (기본 이해를 위해 asm.js로부터 힌트를 얻는다) 브라우저가 최적화하려고 시도 할 수있는 이유와 이유).
이 때문에 성능에 대해 이야기하는 것이 중요하지만 광산 분야 이기도합니다. 그리고 광산 분야 때문에 관련 자료를 가리키고 인용하는 것으로 정말로 끝내고 싶습니다.
존재하지 않는 객체 속성 및 범위를 벗어난 배열 요소에 액세스 undefined
하면 예외를 발생시키는 대신 값이 반환 됩니다. 이러한 동적 기능으로 JavaScript 프로그래밍이 편리하지만 JavaScript를 효율적인 기계 코드로 컴파일하기가 어렵습니다.
...
효과적인 JIT 최적화를위한 중요한 전제는 프로그래머가 체계적인 방식으로 JavaScript의 동적 기능을 사용한다는 것입니다. 예를 들어, JIT 컴파일러는 객체 속성이 특정 유형의 객체에 특정 순서로 추가되거나 경계를 벗어난 어레이 액세스가 거의 발생하지 않는다는 사실을 이용합니다. JIT 컴파일러는 이러한 규칙 성 가정을 활용하여 런타임시 효율적인 기계 코드를 생성합니다. 코드 블록이 가정을 만족하면 JavaScript 엔진은 효율적으로 생성 된 기계 코드를 실행합니다. 그렇지 않으면 엔진이 더 느린 코드 또는 프로그램 해석으로 폴백해야합니다.
출처 :
"JITProf : JIT- 친숙하지 않은 JavaScript 코드의 정확한 위치 지정"
Berkeley 간행물, 2014 년 Liang Gong, Michael Pradel, Koushik Sen.
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS (바운드 배열 액세스를 좋아하지 않음) :
미리 컴파일
asm.js는 JavaScript의 엄격한 하위 집합이므로이 사양은 유효성 검사 논리 만 정의합니다. 실행 의미는 단순히 JavaScript의 의미입니다. 그러나 검증 된 asm.js는 AOT 컴파일에 적합합니다. 또한 AOT 컴파일러가 생성 한 코드는 다음과 같은 특징을 가진 매우 효율적일 수 있습니다.
- 정수 및 부동 소수점 숫자의 박스 화되지 않은 표현;
- 런타임 유형 검사가 없습니다.
- 가비지 수집 부재; 과
- 효율적인 힙로드 및 저장 (플랫폼에 따라 다양한 구현 전략 사용)
검증에 실패한 코드는 해석 및 / 또는 JIT (Just-In-Time) 컴파일과 같은 전통적인 수단으로 실행으로 대체해야합니다.
http://asmjs.org/spec/latest/
그리고 마지막으로 https://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/
는 경계를 제거 할 때 엔진의 내부 성능 향상에 대한 작은 하위 섹션이 있었습니까? 점검 (루프 밖에서 경계 점검을 들어 올리면 이미 40 % 개선되었습니다).
편집 :
여러 소스가 해석에 이르기까지 다양한 수준의 JIT 재 컴파일에 대해 이야기합니다.
OP의 스 니펫에 관한 위의 정보 를 기반으로 한 이론적 예 :
- isPrimeDivisible에 전화
- 일반적인 가정을 사용하여 isPrimeDivisible 컴파일 (범위를 벗어난 액세스 없음)
- 일해
- BAM, 갑자기 배열이 범위를 벗어났습니다 (끝에서 오른쪽으로).
- Crap은 엔진에 따르면 다른 (더 적은) 가정을 사용하여 isPrimeDivisible을 다시 컴파일 하고이 예제 엔진은 현재 부분 결과를 재사용 할 수 있는지 파악하려고 시도하지 않습니다.
- 느린 기능을 사용하여 모든 작업을 다시 계산하십시오.
- 반환 결과
따라서 시간은 다음과 같습니다.
첫 실행 (종료 실패) + 각 반복에 대해 느린 기계 코드를 사용하여 모든 작업을 다시 수행 + 재 컴파일 등. 이 이론적 인 예에서는 분명히 2 배 이상 더 걸립니다 !
편집 2 : (면책 조항 : 아래 사실에 근거한 추측)
더 많이 생각할수록이 답변이 실제로 잘못된 스 니펫 a (또는 스 니펫 b의 성능 보너스)에 대한 '벌금'에 대한 더 지배적 인 이유를 더 많이 설명 할 것이라고 생각합니다 , 어떻게 생각하는지에 따라) 정확하게 프로그래밍 오류라고 부릅니다 (스 니펫 a).
this.primes
'밀집 배열'순수 라고 가정하는 것이 꽤 유혹적 입니다.
- 소스 코드의 하드 코딩 된 리터럴 ( 컴파일 타임 전에 모든 것이 이미 컴파일러에 알려진 것처럼 '실제'배열이 될 수있는 탁월한 후보 ) 또는
- 사전 크기 (
new Array(/*size value*/)
)를 오름차순으로 채우는 숫자 함수 ( '실제'배열이되는 다른 오래 알려진 후보)를 사용하여 생성 된 것 같습니다 .
우리는 또한 알고 primes
배열의 길이가되는 캐시 로 prime_count
! (의도 및 고정 크기임을 나타냄).
또한 대부분의 엔진은 처음에 Array-mod-in-modify (필요한 경우)로 배열을 전달하므로 변경하지 않으면 처리 속도가 훨씬 빨라집니다.
따라서 Array primes
가 내부적으로 최적화 된 배열 이라고 가정하는 것이 합리적입니다 (생성 후 배열을 수정하는 코드가없는 경우 컴파일러에 대해 간단하게 알 수 있음). 최적화 된 방식으로 저장된 엔진), 꽤 많은 것처럼 그것은이었다 Typed Array
.
필자의 sum
함수 예제로 명확하게하려고 시도 했을 때 전달되는 인수는 실제로 발생 해야하는 사항과 특정 코드가 기계 코드로 컴파일되는 방식에 영향을 미칩니다. 함수에 a String
를 전달 sum
하면 문자열이 변경되지 않고 함수가 JIT 컴파일 방식을 변경해야합니다! 배열을 전달하면 기계 코드 sum
의 다른 버전 (아마도이 유형의 경우 추가 또는 '모양' 이라고도 함)을 컴파일해야합니다.
Typed_Array와 같은 primes
Array를 on_the- fly로 something_else 로 변환하는 것은 약간 bonkus처럼 보이지만 컴파일러는이 함수가 수정하지 않을 것이라는 것을 알고 있습니다!
이러한 가정 하에서 두 가지 옵션이 남습니다.
- 범위를 벗어나지 않는다고 가정하고 번호 크 런처로 컴파일하고, 끝에서 범위를 벗어난 문제에 부딪 히고, 재 컴파일 및 재실행 작업 (위의 편집 1의 이론적 예에 설명 된대로)
- 컴파일러가 이미 사전에 바운드 액세스 범위를 벗어 났음을 감지했거나 의심 했습니까? 전달 된 인수가 희소 오브젝트 인 것처럼 함수가 JIT 컴파일되어 기능적인 기계 코드가 느려짐 기타.). 다시 말해, 함수는 특정 최적화에 적합하지 않았으며 마치 'sparse array'(-like) 인수를받는 것처럼 컴파일되었습니다.
나는 지금이 2 중 어느 것이 궁금합니다!
<=
과<
동일합니다.