최적화되지 않은 상태에서 고성능 Javascript 코드 작성


10

큰 숫자 배열 (정수 또는 부동 소수점 숫자에서 작동하는 선형 대수 패키지 생각)에서 작동하는 Javascript로 성능에 민감한 코드를 작성할 때 항상 JIT가 최대한 많은 도움을주기를 원합니다. 대략 이것은 다음을 의미합니다.

  1. 정수 또는 부동 소수점 계산을 수행하는지에 따라 항상 배열을 압축 된 SMI (작은 정수) 또는 압축 된 복식으로 만들고자합니다.
  2. 우리는 항상 같은 유형의 것을 함수에 전달하여 "거대"라는 레이블이 붙지 않고 최적화되지 않도록하고 싶습니다. 예를 들어, 우리는 항상 호출해야 할 vec.add(x, y)모두 xy포장 SMI 배열되는, 또는 둘 모두 이중 배열을 포장.
  3. 함수가 가능한 한 많이 인라인되기를 원합니다.

이러한 경우를 벗어나면 갑자기 급격한 성능 저하가 발생합니다. 이것은 여러 가지 무해한 이유로 발생할 수 있습니다.

  1. 압축 된 SMI 배열을과 같은 겉보기에 무해한 작업을 통해 압축 된 이중 배열로 변환 할 수 있습니다 myArray.map(x => -x). 패킹 된 Double 배열이 여전히 매우 빠르기 때문에 이것은 실제로 "최상의"나쁜 경우입니다.
  2. 당신은 (갑자기) 반환 함수를 통해 배열에 매핑하여, 예를 들어, 일반적인 박스 배열로 포장 된 배열을 설정 할 수 null또는 undefined. 이 나쁜 경우는 피하기가 매우 쉽습니다.
  3. vec.add()너무 많은 유형의 물건을 전달하고 그것을 변형시키는 것과 같은 전체 기능을 최적화하지 않을 수 있습니다 . "일반 프로그래밍"을 수행하려는 경우에 발생할 수 있습니다. 여기서 vec.add()유형에 대해주의를 기울이지 않는 경우 (많은 유형이 표시되는 경우)와 최대 성능을 달성하려는 경우 모두에서 사용됩니다. (예를 들어 박스 복식 만받을 수 있습니다).

내 질문은 위의 고려 사항에 비추어 고성능 Javascript 코드를 작성하는 방법과 코드를 훌륭하고 읽기 쉽게 유지하는 방법에 대한 간단한 질문입니다. 어떤 구체적인 하위 질문을 통해 내가 어떤 종류의 답변을 목표로하는지 알 수 있습니다.

  • 팩형 SMI 어레이의 세계에 머무르면서 프로그래밍하는 방법에 대한 지침이 있습니까?
  • 매크로 사이트와 같은 것을 사용하지 않고 자바 스크립트에서 일반적인 고성능 프로그래밍을 수행 할 수 vec.add()있습니까?
  • 대형 콜 사이트 및 최적화 해제와 같은 관점에서 고성능 코드를 라이브러리로 모듈화하는 방법은 무엇입니까? 예를 들어, Linear Algebra 패키지 A를 고속으로 즐겁게 사용 하고 있고 B에 의존 하는 패키지 를 가져 A오지만 B다른 유형으로 호출하여 최적화를 해제하면 갑자기 코드가 변경되지 않고 코드가 느리게 실행됩니다.
  • 좋은 있는가 사용하기 쉬운 자바 스크립트 엔진이 유형의 내부적으로 무엇을하고 있는지 확인하기위한 측정 도구는?

1
그것은 매우 흥미로운 주제이며, 연구의 일부를 올바르게 수행했음을 보여주는 매우 잘 작성된 게시물입니다. 그러나 나는 그 질문이 SO 형식에 비해 너무 광범위하고 또한 그것이 사실보다 더 많은 의견을 끌 것이라고 두려워합니다. 코드 최적화는 매우 복잡한 문제이며 두 버전의 엔진이 동일하게 작동하지 않을 수 있습니다. V8 JIT를 담당하는 직원 중 한 명이 가끔 멈춰서 엔진에 대한 적절한 답변을 줄 수도 있지만, 심지어 단일 Q / A에 대해서는 너무 광범위한 주제라고 생각합니다. .
카이도

"제 질문은 고성능 자바 스크립트 코드를 작성하는 방법에 대한 간단한 질문입니다."제쳐두고, 자바 스크립트는 백그라운드 프로세스 (웹 워커)를 생성하는 데 도움이된다는 점에 주목하십시오. GPU (tensorflow.js 및 gpu.js) 오퍼링은 자바 스크립트 기반 응용 프로그램의 계산 처리량을 높이기 위해 컴파일에만 의존하는 것 이외의 수단을 의미합니다.
Jon Trent

@JonTrent 사실 나는 내 게시물에 약간 거짓말을 했으므로 고전 선형 대수 응용 프로그램에는 신경 쓰지 않지만 정수보다 컴퓨터 대수에는 더 신경 쓰지 않습니다. 이것은 (예를 들어) 행렬을 행으로 줄이면서 2로 나눌 수 있기 때문에 많은 기존 숫자 패키지가 즉시 배제된다는 것을 의미합니다. 정수가 아닙니다. 웹 작업자를 고려했습니다 (특히 취소 할 수있는 장기 실행 계산의 경우). 여기서 내가 다루고있는 문제는 상호 작용에 응답 할 수있을만큼 대기 시간을 낮추는 것입니다.
Joppy

JavaScript의 정수 산술의 경우, 아마도 " |0모든 연산 뒤에 숨어 "있는 asm.js 스타일 코드를보고있을 것입니다 . 예쁘지는 않지만 적절한 정수가없는 언어로 할 수있는 최선의 방법입니다. BigInts를 사용할 수도 있지만 현재로서는 일반적인 엔진에서 빠르지는 않습니다 (주로 수요 부족으로 인해).
jmrk

답변:


8

여기 V8 개발자. 이 질문에 대한 관심의 정도와 다른 답변의 부족을 감안할 때, 나는 이것을 한 번에 줄 수 있습니다. 그래도 당신이 기대했던 대답이 아닐까 걱정됩니다.

팩형 SMI 어레이의 세계에 머무르면서 프로그래밍하는 방법에 대한 지침이 있습니까?

짧은 대답 : 바로 여기 있습니다 : const guidelines = ["keep your integers small enough"].

더 긴 대답 : 여러 가지 이유로 종합적인 지침 세트를 제공하는 것은 어렵습니다. 일반적으로 JavaScript 개발자는 자신과 사용 사례에 맞는 코드를 작성해야하며 JavaScript 엔진 개발자는 엔진에서 해당 코드를 빠르게 실행하는 방법을 알아야합니다. 반대로, 엔진 구현 선택 및 최적화 노력에 관계없이 일부 코딩 패턴은 다른 코딩 패턴보다 항상 더 높은 성능 비용을 가질 것이라는 점에서 이상적으로는 몇 가지 한계가 있습니다.

성능 조언에 대해 이야기 할 때, 우리는이를 염두에두고 여러 엔진에서 수년 동안 유효한 상태를 유지할 가능성이 높은 권장 사항을 신중하게 평가하며 또한 관용적이고 비 간섭 적입니다.

당장 예제로 돌아 가기 : Smis를 내부적으로 사용하는 것은 사용자 코드가 알 필요가없는 구현 세부 사항으로 간주됩니다. 그것은 어떤 경우를보다 효율적으로 만들 것이고 다른 경우에는 아프지 않아야합니다. 모든 엔진이 Smis를 사용하는 것은 아닙니다. 문제). V8에서 Smis의 크기는 내부 세부 사항이며 실제로 시간이 지남에 따라 버전이 변경되었습니다. 대부분의 유스 케이스였던 32 비트 플랫폼에서 Smis는 항상 31 비트 부호있는 정수였습니다. 64 비트 플랫폼에서는 32 비트 부호있는 정수로 사용되었습니다. Chrome 80에서 '포인터 압축'을 제공 할 때까지 가장 일반적인 경우처럼 보였습니다. 64 비트 아키텍처의 경우 Smi 크기를 32 비트 플랫폼에서 알려진 31 비트로 낮추어야했습니다. Smis가 일반적으로 32 비트라는 가정을 기반으로 구현 한 경우 다음과 같은 불행한 상황이 발생합니다.이것 .

고맙게도, 언급했듯이 이중 배열은 여전히 ​​매우 빠릅니다. 숫자가 많은 코드의 경우 이중 배열을 가정 / 타겟팅하는 것이 좋습니다. JavaScript에서 doubles가 널리 퍼져 있으므로 모든 엔진이 double 및 double 배열을 잘 지원한다고 가정하는 것이 합리적입니다.

매크로 시스템과 같은 것을 사용하지 않고 vec.add ()와 같은 것을 호출 사이트에 인라인하지 않고 Javascript로 일반 고성능 프로그래밍을 수행 할 수 있습니까?

"일반"은 일반적으로 "고성능"과 상충됩니다. 이것은 JavaScript 또는 특정 엔진 구현과 관련이 없습니다.

"일반"코드는 런타임에 결정을 내려야 함을 의미합니다. 함수를 실행할 때마다 " x정수입니까? 그렇다면 해당 코드 경로를 가져 x옵니다. 문자열입니까? 그러면 여기로 건너 뜁니다. 개체입니까 .valueOf? 아니오?" "아마도 .toString()프로토 타입 체인에 있을까요?"라고 부르고 결과부터 처음부터 다시 시작하십시오. " "고성능"최적화 된 코드는 본질적으로 이러한 모든 동적 검사를 중단한다는 아이디어를 기반으로합니다. 그것은 엔진 / 컴파일러가 미리 유형을 미리 추론 할 수있는 방법이있을 때만 가능합니다 : x항상 정수가 될 것임을 증명할 수 있거나 (또는 ​​충분히 높은 확률로 가정 할 경우) 해당 사례에 대한 코드 만 생성하면됩니다 ( 입증되지 않은 가정이 포함 된 경우 유형 검사로 보호됩니다.

인라인은이 모든 것에 직교합니다. "일반적인"함수는 여전히 인라인 될 수 있습니다. 경우에 따라 컴파일러는 형식 정보를 인라인 함수로 전파하여 다형성을 줄일 수 있습니다.

(비교를 위해 : 정적으로 컴파일 된 언어 인 C ++에는 관련 문제를 해결하기위한 템플릿이 있습니다. 간단히 말해서, 프로그래머는 컴파일러가 주어진 유형에 대해 매개 변수화 된 함수 (또는 전체 클래스)의 특수한 사본을 작성하도록 컴파일러에 명시 적으로 지시 할 수 있습니다. 긴 컴파일 시간과 큰 이진과 같은 자체 단점이없는 경우도 있지만, 자바 스크립트는 템플릿과 같은 것이 없기 eval때문에 다소 유사한 시스템을 구축하는 데 사용할 수 있습니다. 비슷한 단점이 있습니다 : 런타임에 C ++ 컴파일러의 작업과 동등한 작업을 수행해야하며 생성하는 코드의 양에 대해 걱정해야합니다.)

대형 콜 사이트 및 최적화 해제와 같은 관점에서 고성능 코드를 라이브러리로 모듈화하는 방법은 무엇입니까? 예를 들어, Linear Algebra 패키지 A를 고속으로 행복하게 사용하고 A에 의존하는 패키지 B를 가져 오지만 B는 다른 유형으로 호출하여 최적화하지 않고 갑자기 코드를 변경하지 않고 코드를 느리게 실행합니다 .

예, 이는 JavaScript의 일반적인 문제입니다. V8 Array.sort은 JavaScript에서 내부적으로 특정 내장 (예 :)을 구현하는 데 사용되었으며이 문제 ( "유형 피드백 오염"이라고 함)는이 기술에서 완전히 벗어난 주된 이유 중 하나였습니다.

즉, 숫자 코드의 경우 많은 유형 (Smis 및 double 만)이 아니며 실제로 언급 한 바와 같이 실제로 유사한 성능을 가져야하므로 유형 피드백 오염은 실제로 이론적 인 문제이며 경우에 따라 유의미한 영향을 미치면 선형 대수 시나리오에서 측정 가능한 차이가 나타나지 않을 가능성이 상당히 높습니다.

또한 엔진 내부에는 "하나의 유형 == 빠름"및 "하나 이상의 유형 == 느림"보다 더 많은 상황이 있습니다. 주어진 작업에서 Smis와 Doubles가 모두 보이면 완전히 괜찮습니다. 두 종류의 배열에서 요소를로드하는 것도 좋습니다. 우리는 부하가 개별적으로 추적 할 때 너무 많은 다른 유형을 보았을 때 상황에 대해 "비정형 적"이라는 용어를 사용하고 대신 많은 유형으로 더 잘 확장되는보다 일반적인 메커니즘을 사용합니다. 여전히 최적화됩니다. "비 최적화"는 이전에 보지 못했던 새로운 유형이 보이고 최적화 된 코드가 처리 할 수 ​​없기 때문에 함수에 대해 최적화 된 코드를 버리는 매우 구체적인 행위입니다. 그러나 그조차도 괜찮습니다. 최적화되지 않은 코드로 돌아가서 더 많은 유형 피드백을 수집하고 나중에 다시 최적화하십시오. 이 과정이 두 번 발생하면 걱정할 것이 없습니다. 병적으로 나쁜 경우에만 문제가됩니다.

따라서 모든 요약은 걱정하지 마십시오 . 합리적인 코드를 작성하고 엔진이 처리하도록하십시오. 그리고 "합리적"이란 말은 유스 케이스에 적합한 것은 읽기 쉽고 유지 관리가 가능하며 효율적인 알고리즘을 사용하며 배열의 길이를 넘어서 읽는 것과 같은 버그를 포함하지 않는 것입니다. 이상적으로는 그게 전부이며 다른 것을 할 필요가 없습니다. 무언가 를하는 것이 더 나아지 거나 실제로 성능 문제를 관찰하고 있다면 두 가지 아이디어를 제안 할 수 있습니다.

TypeScript 사용하면 도움이 될 수 있습니다 . 뚱뚱한 경고 : TypeScript의 유형은 실행 성능이 아닌 개발자 생산성을 목표로합니다 (이 두 관점은 유형 시스템과 요구 사항이 매우 다름). 예를 들어으로 일관되게 주석을 달면 number실수 null로 숫자 만 포함 / 연산 해야하는 배열이나 함수에 실수로 삽입하면 TS 컴파일러가 경고합니다 . 물론, 훈련은 여전히 ​​필요합니다. number_func(random_object as number)유형 주석의 정확성이 어느 곳에도 적용되지 않기 때문에 단일 탈출 해치가 모든 것을 자동으로 손상시킬 수 있습니다.

TypedArray를 사용하면 도움이 될 수 있습니다. 일반 JavaScript 배열에 비해 배열 당 오버 헤드 (메모리 소비 및 할당 속도)가 약간 더 많으므로 (작은 배열이 많은 경우 일반 배열이 더 효율적일 수 있음) 확장 할 수 없기 때문에 유연성이 떨어집니다. 또는 할당 후 축소되지만 모든 요소가 정확히 하나의 유형을 갖도록 보장합니다.

Javascript 엔진이 유형에 대해 내부적으로 수행중인 작업을 확인하는 데 사용하기 쉬운 측정 도구가 있습니까?

아니, 그건 의도적이야 위에서 설명했듯이, V8이 오늘날 특히 잘 최적화 할 수있는 패턴에 맞게 코드를 구체적으로 맞춤화하고 싶지는 않으며 실제로 그렇게하고 싶지도 않습니다. 사용하려는 패턴이있는 경우 향후 버전에서이를 최적화 할 수 있습니다 (이전에는 언 박스형 32 비트 정수를 배열 요소로 저장한다는 아이디어로 이전했습니다.) 그러나 아직 연구가 시작되지 않았으므로 약속은 없습니다.); 때로는 과거에 최적화하는 데 사용했던 패턴이있는 경우 더 중요하고 효과적인 다른 최적화 방법을 사용하지 않는 경우 해당 패턴을 제거하기로 결정할 수 있습니다. 또한 휴리스틱 인라이닝과 같은 것은 제대로 이해하기가 어렵습니다. 적절한 시점에 올바른 인라인 결정을 내리는 것은 지속적인 연구와 엔진 / 컴파일러 동작에 대한 해당 변경 영역입니다. 이것은 모두에게 불행한 또 다른 경우입니다.그리고 우리) 당신이 현재 브라우저 버전 중 일부가 대략 당신이 생각하는 (또는 알고 있습니까?) 최고의 결정을 내릴 때까지 코드를 조정하는 데 많은 시간을 소비했다면, 반 년 후에 다시 돌아와서 그 당시의 브라우저를 깨닫기 만하면됩니다. 휴리스틱을 변경했습니다.

물론 애플리케이션 전체의 성능을 항상 전체적으로 측정 할 수 있습니다. 즉, 엔진이 특별히 내부적으로 선택한 것이 아니라 궁극적으로 중요한 것입니다. 마이크로 벤치 마크에주의하십시오. 오해의 소지가 있습니다. 두 줄의 코드 만 추출하여 벤치마킹하면 엔진이 매우 다른 결정을 내릴 수 있도록 시나리오가 충분히 다를 수 있습니다 (예 : 다른 유형 피드백).


2
이 훌륭한 답변을 주셔서 감사합니다, 그것은 사물이 작동하는 방법에 대한 나의 의심의 많은을 확인하고, 그들이있어 중요한 것은 어떻게 구성 작업에. 그건 그렇고, 당신이 언급 한 "유형 피드백"문제에 관한 블로그 게시물이 Array.sort()있습니까? 나는 그것에 대해 조금 더 읽고 싶습니다.
Joppy

우리가 그 특정 측면에 대해 블로그를 작성했다고 생각하지 않습니다. 본질적으로 질문에 직접 설명 한 것입니다. 내장이 JavaScript로 구현되면 다른 코드 조각이 다른 유형으로 코드를 호출하면 성능이 저하 될 수 있다는 의미에서 "라이브러리와 같습니다". 때때로 더. 그것은 그 기술에 대한 유일한 문제는 아니었고, 아마도 가장 큰 문제는 아니었다. 나는 주로 일반적인 문제에 익숙하다고 말하고 싶었습니다.
jmrk
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.