JavaScript でループ展開しますか?

王林
リリース: 2024-07-24 13:18:52
オリジナル
756 人が閲覧しました

Loop Unrolling in JavaScript?

JavaScript can feel very removed from the hardware it runs on, but thinking low-level can still be useful in limited cases.

A recent post of Kafeel Ahmad on loop optimization detailed a number of loop performance improvement techniques. That article got me thinking about the topic.

Premature Optimization

Just to get this out of the way, this is a technique very few will ever need to consider in web development. Also, focusing on optimization too early can make code harder to write and much harder maintain. Taking a peek at low-level techniques can give us insight into our tools and the work in general, even if we can't apply that knowledge directly.

What is Loop Unrolling?

Loop unrolling basically duplicates the logic inside a loop so you perform multiple operations during each, well, loop. In specific cases, making the code in the loop longer can make it faster.

By intentionally performing some operations in groups rather than one-by-one, the computer may be able to operate more efficiently.

Unrolling Example

Let's take a very simple example: summing values in an array.

// 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;
};
ログイン後にコピー

This may look very strange at first. We're managing more variables and performing additional operations that don't happen in the simple example. How can this be faster?!

Measuring the Difference

I ran some comparisons over a variety of data sizes and multiple runs, as well as sequential or interleaved testing. The parallelSum performance varied, but was almost always better, excepting some odd results for very small data sizes. I tested this using RunJS, which is built on Chrome's V8 engine.

Different data sizes gave very roughly these results:

  • Small (< 10k): Rarely any difference
  • Medium (10k-100k): Typically ~20-80% faster
  • Large (> 1M): Consistently twice as fast

    Then I created a JSPerf with 1 million records to try across different browsers. Try it yourself!

    Chrome ran parallelSum twice as fast as simpleSum, as expected from the RunJS testing.

    Safari was almost identical to Chrome, both in percents and operations per second.

    Firefox on the same system performed almost the same for simpleSum but parallelSum was only about 15% faster, not twice as fast.

    This variation sent me looking for more information. While it's nothing definitive, I found a StackOverflow comment from 2016 discussing some of the JS engine issues with loop unrolling. It's an interesting look at how engines and optimizations can affect code in ways we don't expect.

    Variations

    I tried a third version as well, which added two values in a single operation to see if there was a noticeable difference between one variable and two.

    const parallelSum = (data) => {
      let sum = 0
      for(let i=0; i < data.length; i += 2) {
        sum += data[i] + data[i + 1];
      }
      return sum;
    };
    
    ログイン後にコピー

    Short answer: No. The two "parallel" versions were within the reported margin of error of each other.

    So, How Does it Work?

    While JavaScript is single-threaded, the interpreters, compilers, and hardware underneath can perform optimizations for us when certain conditions are met.

    In the simple example, the operation needs the value i to know what data to fetch, and it needs the latest value of sum to update. Because both of these change in each loop, the computer has to wait for the loop to complete to get more data. While it may seem obvious to us what i += 1 will do, the computer mostly understands "the value will change, check back later", so it has difficulty optimizing.

    Our parallel versions load multiple data entries for each value of i. We still depend on sum for each loop, but we can load and process twice as much data per cycle. But that doesn't mean it runs twice as fast.

    Deeper Dive

    To understand why loop unrolling works we look to the low-level operation of a computer. Processors with super-scalar architectures can have multiple pipelines to perform simultaneous operations. They can support out-of-order execution so operations that don't depend on each other can happen as soon as possible. For some operations, SIMD can perform one action on multiple pieces of data at once. Beyond that we start getting into caching, data fetching, and branch prediction...

    But this is a JavaScript article! We're not going that deep. If you want to know more about processor architectures, Anandtech has some excellent Deep Dives.

    Had dan Kelemahan

    Membuka gelung bukan sihir. Terdapat had dan pulangan yang semakin berkurangan yang muncul kerana saiz program atau data, kerumitan operasi, seni bina komputer dan banyak lagi. Tetapi kami hanya menguji satu atau dua operasi dan komputer moden selalunya menyokong empat atau lebih rangkaian.

    Untuk mencuba beberapa kenaikan yang lebih besar, saya membuat JSPerf lain dengan 1, 2, 4, dan 10 rekod dan menjalankannya pada Apple M1 Max MacBook Pro yang menjalankan macOS 14.5 Sonoma, dan PC AMD Ryzen 9 3950X yang menjalankan Windows 11.

    Sepuluh rekod pada satu masa adalah 2.5-3.5x lebih pantas daripada gelung asas, tetapi hanya 12-15% lebih pantas daripada memproses empat rekod pada Mac. Pada PC kami masih melihat peningkatan 2x ganda antara satu hingga dua rekod, tetapi sepuluh rekod hanya 2% lebih pantas daripada empat rekod, yang saya tidak akan ramalkan untuk pemproses 16 teras.

    Platform dan Kemas Kini

    Hasil yang berbeza ini mengingatkan kita untuk berhati-hati dengan pengoptimuman. Mengoptimumkan komputer anda boleh mencipta pengalaman yang lebih buruk pada perkakasan yang kurang berkemampuan atau hanya berbeza. Isu prestasi atau kefungsian untuk perkakasan yang lebih lama atau peringkat permulaan ialah isu biasa apabila pembangun mengusahakan mesin yang pantas dan berkuasa, dan ia adalah sesuatu yang saya telah ditugaskan beberapa kali dalam kerjaya saya.

    Untuk beberapa skala prestasi, Chromebook peringkat permulaan yang tersedia pada masa ini daripada HP mempunyai pemproses Intel Celeron N4120. Ini kira-kira bersamaan dengan 2013 Core i5-4250U MacBook Air saya. Ia hanya mempunyai satu kesembilan prestasi M1 Max dalam penanda aras sintetik. Pada MacBook Air 2013 itu, menjalankan versi terkini Chrome, fungsi 4 rekod lebih pantas daripada 10 rekod, tetapi masih hanya 60% lebih pantas daripada fungsi rekod tunggal!

    Pelayar dan piawaian juga sentiasa berubah. Kemas kini penyemak imbas rutin atau seni bina pemproses yang berbeza boleh menjadikan kod yang dioptimumkan lebih perlahan berbanding gelung biasa. Apabila anda mendapati diri anda mengoptimumkan secara mendalam, anda mungkin perlu memastikan pengoptimuman anda adalah relevan kepada pengguna anda, dan ia kekal relevan.

    Ia mengingatkan saya kepada buku JavaScript Prestasi Tinggi oleh Nicholas Zakas, yang saya baca pada tahun 2012. Ia adalah buku yang hebat dan mengandungi banyak pandangan. Walau bagaimanapun, menjelang 2014 beberapa isu prestasi penting yang dikenal pasti dalam buku itu telah diselesaikan atau dikurangkan dengan ketara oleh kemas kini enjin penyemak imbas dan kami dapat menumpukan lebih banyak usaha untuk menulis kod yang boleh diselenggara.

    Jika anda cuba untuk kekal pada kelebihan pengoptimuman prestasi, bersedia untuk perubahan dan pengesahan tetap.

    Pengajaran dari Masa Lalu

    Semasa meneliti topik ini, saya menjumpai utas Linux Kernel Mailing List dari tahun 2000 tentang mengalih keluar beberapa pengoptimuman pembukaan gelung yang akhirnya meningkatkan prestasi aplikasi. Ia termasuk perkara yang masih relevan ini (penekanan saya):

    Intinya ialah andaian intuitif kami tentang apa yang pantas dan apa yang tidak boleh selalunya salah, terutamanya memandangkan berapa banyak CPU telah berubah sejak beberapa tahun lalu.
    – Theodore Ts'o

    Kesimpulan

    Ada masanya anda mungkin perlu memerah prestasi daripada satu gelung, dan jika anda memproses item yang mencukupi, ini boleh menjadi salah satu cara anda melakukannya. Adalah baik untuk mengetahui tentang pengoptimuman jenis ini, tetapi untuk kebanyakan kerja, You Aren't Gonna Need It™.

    Namun saya harap anda telah menikmati cerita saya dan mungkin pada masa hadapan ingatan anda akan digerakkan tentang pertimbangan pengoptimuman prestasi.

    Terima kasih kerana membaca!

    以上がJavaScript でループ展開しますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!