>  기사  >  웹 프론트엔드  >  JavaScript에서 루프 풀기?

JavaScript에서 루프 풀기?

王林
王林원래의
2024-07-24 13:18:52940검색

Loop Unrolling in JavaScript?

JavaScript는 실행되는 하드웨어에서 매우 동떨어진 느낌을 받을 수 있지만 제한된 경우에는 낮은 수준으로 생각하는 것이 여전히 유용할 수 있습니다.

루프 최적화에 관한 Kafeel Ahmad의 최근 게시물에서는 다양한 루프 성능 개선 기술을 자세히 설명했습니다. 그 기사를 보고 그 주제에 대해 생각하게 되었습니다.

조기 최적화

이 문제를 해결하기 위해 웹 개발에서 고려해야 할 기술은 거의 없습니다. 또한 너무 일찍 최적화에 집중하면 코드 작성이 더 어려워지고 유지 관리가 훨씬 더 어려워질 수 있습니다. 해당 지식을 직접 적용할 수 없더라도 낮은 수준의 기술을 살펴보면 도구와 작업 전반에 대한 통찰력을 얻을 수 있습니다.

루프 언롤링이란 무엇입니까?

루프 풀기는 기본적으로 루프 내부의 논리를 복제하므로 각 루프 동안 여러 작업을 수행합니다. 특별한 경우에는 루프의 코드를 길게 만들면 더 빠르게.

일부 작업을 하나씩 수행하는 것이 아니라 의도적으로 그룹으로 수행함으로써 컴퓨터가 더 효율적으로 작동할 수 있습니다.

언롤링 예

아주 간단한 예를 들어보겠습니다. 배열의 값을 합산하는 것입니다.

// 1-to-1 looping
const simpleSum = (data) => {
  let sum = 0;
  for(let i=0; i < data.length; i += 1) {
    sum += data[i];
  }
  return sum;
};

const parallelSum = (data) => {
  let sum1 = 0;
  let sum2 = 0;
  for(let i=0; i < data.length; i += 2) {
    sum1 += data[i];
    sum2 += data[i + 1];
  }
  return sum1 + sum2;
};

처음에는 매우 이상하게 보일 수 있습니다. 우리는 더 많은 변수를 관리하고 간단한 예에서는 발생하지 않는 추가 작업을 수행하고 있습니다. 어떻게 이렇게 빨라질 수 있나요?!

차이 측정

다양한 데이터 크기와 다중 실행은 물론 순차 또는 인터리브 테스트를 통해 몇 가지 비교를 실행했습니다. ParallelSum 성능은 다양했지만 매우 작은 데이터 크기에 대한 일부 이상한 결과를 제외하면 거의 항상 더 좋았습니다. Chrome의 V8 엔진을 기반으로 구축된 RunJS를 사용하여 테스트했습니다.

다양한 데이터 크기로 인해 대략 다음과 같은 결과가 나왔습니다.

  • 소형(< 10,000): 거의 차이 없음
  • 중간(10k-100k): 일반적으로 ~20-80% 더 빠릅니다
  • 대형(> 1M): 지속적으로 두 배 빠른 속도

    그런 다음 다양한 브라우저에서 시도하기 위해 100만 개의 레코드가 포함된 JSPerf를 만들었습니다. 직접 시도해 보세요!

    Chrome은 RunJS 테스트에서 예상한 대로 simpleSum보다 두 배 빠른 속도로 ParallelSum을 실행했습니다.

    Safari는 백분율과 초당 작업 수 측면에서 Chrome과 거의 동일했습니다.

    동일한 시스템의 Firefox는 simpleSum에 대해 거의 동일한 성능을 발휘했지만 ParallelSum은 약 15%만 빨랐고 두 배는 아니었습니다.

    이 변형으로 인해 더 많은 정보를 찾게 되었습니다. 확실한 것은 아니지만 루프 풀기와 관련된 JS 엔진 문제 중 일부를 논의하는 2016년의 StackOverflow 주석을 발견했습니다. 엔진과 최적화가 우리가 예상하지 못한 방식으로 코드에 어떤 영향을 미칠 수 있는지에 대한 흥미로운 시각입니다.

    변형

    한 변수와 두 변수 사이에 눈에 띄는 차이가 있는지 확인하기 위해 한 번의 작업으로 두 값을 추가하는 세 번째 버전도 시도했습니다.

    const parallelSum = (data) => {
      let sum = 0
      for(let i=0; i < data.length; i += 2) {
        sum += data[i] + data[i + 1];
      }
      return sum;
    };
    

    짧은 답변: 아니요. 두 개의 "병렬" 버전은 서로 보고된 오류 범위 내에 있었습니다.

    그렇다면 어떻게 작동하나요?

    JavaScript는 단일 스레드이지만 특정 조건이 충족되면 기본 인터프리터, 컴파일러 및 하드웨어가 최적화를 수행할 수 있습니다.

    간단한 예에서 작업은 어떤 데이터를 가져올지 알기 위해 i 값이 필요하고, 업데이트하려면 최신 sum 값이 필요합니다. 각 루프에서 이러한 두 가지 변경 사항이 모두 변경되므로 컴퓨터는 더 많은 데이터를 얻으려면 루프가 완료될 때까지 기다려야 합니다. i += 1이 수행할 작업이 우리에게는 명백해 보일 수 있지만 컴퓨터는 대부분 "값이 변경됩니다. 나중에 다시 확인하세요"라고 이해하므로 최적화하는 데 어려움이 있습니다.

    저희 병렬 버전은 각 i 값에 대해 여러 데이터 항목을 로드합니다. 여전히 각 루프의 합계에 의존하지만 주기당 두 배의 데이터를 로드하고 처리할 수 있습니다. 그러나 이것이 두 배 빠른 실행을 의미하는 것은 아닙니다.

    심층 분석

    루프 풀기가 작동하는 이유를 이해하기 위해 컴퓨터의 하위 수준 작동을 살펴봅니다. 슈퍼 스칼라 아키텍처를 갖춘 프로세서에는 동시 작업을 수행하기 위한 여러 파이프라인이 있을 수 있습니다. 서로 의존하지 않는 작업이 가능한 한 빨리 발생할 수 있도록 비순차적 실행을 지원할 수 있습니다. 일부 작업의 경우 SIMD는 여러 데이터에 대해 동시에 하나의 작업을 수행할 수 있습니다. 그 외에도 캐싱, 데이터 가져오기 및 분기 예측을 시작합니다...

    하지만 이것은 JavaScript 기사입니다! 우리는 그렇게 깊이 들어가지 않을 것입니다. 프로세서 아키텍처에 대해 더 알고 싶다면 Anandtech의 훌륭한 심층 분석을 참조하세요.

    한계와 단점

    루프 풀기는 마법이 아닙니다. 프로그램이나 데이터 크기, 작업 복잡성, 컴퓨터 아키텍처 등으로 인해 나타나는 한계와 수익 감소가 있습니다. 하지만 우리는 한두 가지 작업만 테스트했으며 최신 컴퓨터는 종종 네 개 이상의 스레드를 지원합니다.

    좀 더 큰 증분을 시도하기 위해 1, 2, 4, 10개의 레코드로 또 다른 JSPerf를 만들어 macOS 14.5 Sonoma를 실행하는 Apple M1 Max MacBook Pro와 Windows 11을 실행하는 AMD Ryzen 9 3950X PC에서 실행했습니다.

    한 번에 10개의 레코드를 처리하는 것은 기본 루프보다 2.5~3.5배 빠르지만 Mac에서 4개의 레코드를 처리하는 것보다는 12~15% 더 빠릅니다. PC에서는 여전히 1~2개의 레코드 사이에서 2배의 개선이 있었지만 10개의 레코드는 4개의 레코드보다 단지 2% 더 빨랐습니다. 이는 16코어 프로세서에서는 예측할 수 없었던 것입니다.

    플랫폼 및 업데이트

    이러한 다양한 결과는 최적화에 주의해야 함을 상기시켜 줍니다. 컴퓨터에 맞게 최적화하면 성능이 떨어지거나 다른 하드웨어에서 더 나쁜 경험이 발생할 수 있습니다. 구형 또는 보급형 하드웨어의 성능 또는 기능 문제는 개발자가 빠르고 강력한 시스템을 사용하여 작업할 때 흔히 발생하는 문제이며, 제가 경력을 쌓는 동안 여러 번 이러한 문제를 겪었습니다.

    일부 성능 측면에서 현재 사용 가능한 HP의 보급형 Chromebook에는 Intel Celeron N4120 프로세서가 탑재되어 있습니다. 이것은 내 2013 Core i5-4250U MacBook Air와 대략 동일합니다. 종합 벤치마크에서 M1 Max의 성능은 9분의 1에 불과합니다. 최신 버전의 Chrome을 실행하는 2013 MacBook Air에서 4 레코드 기능은 10 레코드보다 빨랐지만 여전히 단일 레코드 기능보다 60% 더 빨랐습니다!

    브라우저와 표준도 끊임없이 변화하고 있습니다. 정기적인 브라우저 업데이트나 다른 프로세서 아키텍처로 인해 최적화된 코드가 일반 루프보다 느리게 될 수 있습니다. 심층적인 최적화를 수행하고 있다면 최적화가 소비자와 관련이 있고 관련성을 유지하는지 확인해야 할 수 있습니다.

    2012년에 읽은 Nicholas Zakas의 High Performance JavaScript라는 책이 생각납니다. 훌륭한 책이었고 많은 통찰력이 담겨 있었습니다. 그러나 2014년에는 책에서 확인된 여러 가지 중요한 성능 문제가 브라우저 엔진 업데이트를 통해 해결되거나 크게 줄어들었으며 유지 관리 가능한 코드를 작성하는 데 더 많은 노력을 집중할 수 있었습니다.

    성능 최적화의 선두에 있으려면 변화와 정기적인 검증에 대비하세요.

    과거로부터의 교훈

    이 주제를 조사하는 동안 저는 2000년에 작성된 일부 루프 언롤링 최적화를 제거하여 궁극적으로 애플리케이션 성능을 향상시키는 Linux 커널 메일링 목록 스레드를 발견했습니다. 여기에는 여전히 관련성이 높은 다음 사항이 포함되어 있습니다(강조):

    결론은 빠른 것과 그렇지 않은 것에 대한 우리의 직관적인 가정이 틀릴 수 있다는 것입니다. 특히 지난 몇 년 동안 CPU가 얼마나 많이 변경되었는지 고려할 때 더욱 그렇습니다.
    – 테오도르 쵸

    결론

    루프에서 성능을 짜내야 하는 경우가 있으며, 충분한 항목을 처리하고 있다면 이것이 그렇게 하는 방법 중 하나일 수 있습니다. 이러한 종류의 최적화에 대해 알아두면 좋지만 대부분의 작업에서는 You Are n't Gonna Need It™이 필요합니다.

    그래도 제 장황한 설명이 즐거웠기를 바랍니다. 아마도 미래에는 성능 최적화 고려 사항에 대해 기억이 나실 것입니다.

    읽어주셔서 감사합니다!

    위 내용은 JavaScript에서 루프 풀기?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.