باز کردن حلقه در جاوا اسکریپت؟ – انجمن DEV

Summarize this content to 400 words in Persian Lang
جاوا اسکریپت میتواند از سختافزاری که روی آن اجرا میشود بسیار حذف شده باشد، اما تفکر در سطح پایین همچنان میتواند در موارد محدود مفید باشد.
پست اخیر Kafeel Ahmad در مورد بهینه سازی حلقه، تعدادی از تکنیک های بهبود عملکرد حلقه را شرح داده است. آن مقاله مرا در مورد موضوع فکر کرد.
بهینه سازی زودرس
فقط برای از بین بردن این موضوع، این تکنیکی است که تعداد کمی از آنها نیاز به در نظر گرفتن در توسعه وب دارند. همچنین، تمرکز روی بهینهسازی خیلی زود میتواند نوشتن کد را سختتر و نگهداری آن را سختتر کند. نگاهی به تکنیکهای سطح پایین میتواند به ما بینشی نسبت به ابزارها و کار به طور کلی بدهد، حتی اگر نتوانیم آن دانش را مستقیماً به کار ببریم.
Loop Unrolling چیست؟
باز کردن حلقه اساساً منطق درون یک حلقه را کپی می کند، بنابراین شما چندین عملیات را در طول هر یک انجام می دهید، خوب، حلقه. در موارد خاص، ساخت کد در حلقه طولانی تر می تواند آن را بسازد سریعتر.
با انجام عمدی برخی عملیات در گروه ها به جای یک به یک، کامپیوتر ممکن است بتواند کارآمدتر کار کند.
مثال باز کردن
بیایید یک مثال بسیار ساده بگیریم: جمع کردن مقادیر در یک آرایه.
// 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;
};
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
این ممکن است در ابتدا بسیار عجیب به نظر برسد. ما متغیرهای بیشتری را مدیریت می کنیم و عملیات اضافی را انجام می دهیم که در مثال ساده اتفاق نمی افتد. چگونه می تواند این سریع تر باشد؟!
اندازه گیری تفاوت
من چند مقایسه را روی انواع مختلف انجام دادم data اندازهها و اجراهای متعدد، و همچنین آزمایشهای متوالی یا میانلایهای. این parallelSum عملکرد متفاوت بود، اما تقریباً همیشه بهتر بود، به جز برخی از نتایج عجیب و غریب برای اندازههای داده بسیار کوچک. من این را با استفاده از RunJS آزمایش کردم که بر روی موتور V8 کروم ساخته شده است.
اندازه داده های مختلف داده است خیلی تقریبی این نتایج:
کم اهمیت (
متوسط (100-100k): معمولاً 20-80٪ سریعتر است
بزرگ (> 1M): به طور مداوم دو برابر سریعتر
سپس یک JSPerf با 1 میلیون رکورد ایجاد کردم تا در مرورگرهای مختلف امتحان کنم. خودت آن را امتحان کن!
کروم اجرا شد parallelSum دو برابر سریعتر از simpleSumهمانطور که از تست RunJS انتظار می رود.
سافاری هم از نظر درصد و هم از نظر عملیات در ثانیه تقریباً مشابه کروم بود.
فایرفاکس روی همان سیستم تقریباً همین کار را انجام داد simpleSum ولی parallelSum فقط حدود 15 درصد سریعتر بود، نه دو برابر.
این تنوع مرا به دنبال اطلاعات بیشتر فرستاد. در حالی که هیچ چیز قطعی نیست، من یک نظر StackOverflow از سال 2016 پیدا کردم که در مورد برخی از مشکلات موتور JS با باز کردن حلقه بحث می کرد. این نگاه جالبی است به اینکه چگونه موتورها و بهینهسازیها میتوانند بر روی کد تأثیر بگذارند به روشهایی که ما انتظار نداریم.
تغییرات
من یک نسخه سوم را نیز امتحان کردم، که دو مقدار را در یک عملیات اضافه کرد تا ببینم آیا تفاوت محسوسی بین یک متغیر و دو وجود دارد یا خیر.
const parallelSum = (data) => {
let sum = 0
for(let i=0; i data.length; i += 2) {
sum += data[i] + data[i + 1];
}
return sum;
};
وارد حالت تمام صفحه شوید
از حالت تمام صفحه خارج شوید
پاسخ کوتاه: خیر. دو نسخه «موازی» در محدوده خطای گزارش شده یکدیگر قرار داشتند.
بنابراین، چگونه کار می کند؟
در حالی که جاوا اسکریپت تک رشتهای است، مفسرها، کامپایلرها و سختافزار زیر میتوانند در صورت وجود شرایط خاص، بهینهسازیهایی را برای ما انجام دهند.
در مثال ساده، عملیات به مقدار نیاز دارد i برای اینکه بدانید چه دادههایی باید واکشی شوند، و به آخرین مقدار نیاز دارد sum برای به روز رسانی. از آنجایی که هر دوی اینها در هر حلقه تغییر می کند، کامپیوتر باید منتظر باشد تا حلقه کامل شود تا داده های بیشتری دریافت کند. در حالی که ممکن است برای ما واضح به نظر برسد که چه چیزی i += 1 این کار را انجام می دهد، کامپیوتر بیشتر می فهمد “مقدار تغییر خواهد کرد، بعداً بررسی کنید”، بنابراین در بهینه سازی مشکل دارد.
نسخه های موازی ما چندین ورودی داده را برای هر مقدار بارگذاری می کنند i. ما هنوز به آن وابسته هستیم sum برای هر حلقه، اما میتوانیم دو برابر بیشتر داده در هر چرخه بارگذاری و پردازش کنیم. اما این بدان معنا نیست که اجرا می شود دو برابر سریعتر.
شیرجه عمیق تر
برای درک اینکه چرا باز کردن حلقه کار می کند، ما به عملکرد سطح پایین یک کامپیوتر نگاه می کنیم. پردازنده هایی با معماری فوق اسکالر می توانند چندین خط لوله برای انجام عملیات همزمان داشته باشند. آنها می توانند از اجرای خارج از دستور پشتیبانی کنند تا عملیاتی که به یکدیگر وابسته نیستند در اسرع وقت انجام شوند. برای برخی از عملیات، SIMD می تواند یک عمل را روی چندین قطعه داده به طور همزمان انجام دهد. فراتر از آن، ما شروع به ورود به کش، واکشی داده ها و پیش بینی شاخه می کنیم…
اما این یک مقاله جاوا اسکریپت است! ما آنقدر عمیق نمی رویم. اگر می خواهید در مورد معماری پردازنده بیشتر بدانید، Anandtech دارای برخی از Deep Dives عالی است.
محدودیت ها و معایب
باز کردن حلقه جادویی نیست. محدودیتها و بازدههای رو به کاهشی وجود دارد که به دلیل اندازه برنامه یا داده، پیچیدگی عملیات، معماری رایانه و موارد دیگر ظاهر میشوند. اما ما فقط یک یا دو عملیات را آزمایش کردهایم و رایانههای مدرن اغلب از چهار رشته یا بیشتر پشتیبانی میکنند.
برای امتحان برخی افزایشهای بزرگتر، یک JSPerf دیگر با رکوردهای 1، 2، 4 و 10 ساختم و آن را روی Apple M1 Max MacBook Pro با macOS 14.5 Sonoma و یک رایانه AMD Ryzen 9 3950X با ویندوز 11 اجرا کردم.
ده رکورد در یک زمان 2.5-3.5 برابر سریعتر از حلقه پایه بود، اما فقط 12-15٪ سریعتر از پردازش چهار رکورد در مک بود. در رایانه شخصی ما هنوز شاهد بهبود 2 برابری بین یک تا دو رکورد بودیم، اما ده رکورد فقط 2٪ سریعتر از چهار رکورد بود، که من برای یک پردازنده 16 هسته ای پیش بینی نمی کردم.
پلتفرم ها و به روز رسانی ها
این نتایج متفاوت به ما یادآوری می کند که مراقب بهینه سازی باشیم. بهینه سازی برای رایانه شما می تواند تجربه بدتری را در سخت افزارهای کم توان یا فقط متفاوت ایجاد کند. مشکلات مربوط به عملکرد یا عملکرد سخت افزارهای قدیمی یا سطح ابتدایی یک مشکل رایج زمانی است که توسعه دهندگان بر روی ماشین های سریع و قدرتمند کار می کنند، و این چیزی است که من چندین بار در حرفه خود وظیفه انجام آن را داشته ام.
برای برخی از مقیاسهای عملکرد، یک Chromebook سطح پایه فعلی موجود از HP دارای پردازنده Intel Celeron N4120 است. این تقریباً معادل Core i5-4250U MacBook Air 2013 من است. فقط دارد یک نهم عملکرد M1 Max در یک معیار مصنوعی. در مک بوک ایر 2013 که آخرین نسخه کروم را اجرا می کند، تابع 4 رکورد سریعتر از رکورد 10 بود، اما هنوز فقط 60٪ سریعتر از عملکرد تک رکورد بود!
مرورگرها و استانداردها نیز دائما در حال تغییر هستند. یک بهروزرسانی معمول مرورگر یا معماری متفاوت پردازنده میتواند کد بهینهسازی را ایجاد کند آرام تر از یک حلقه معمولی هنگامی که متوجه شدید که عمیقاً در حال بهینه سازی هستید، ممکن است نیاز داشته باشید که بهینه سازی خود را به مصرف کنندگان خود مرتبط و مرتبط باشد مرتبط باقی می ماند.
این کتاب من را به یاد کتاب جاوا اسکریپت با کارایی بالا اثر نیکلاس زکاس می اندازد که در سال 2012 خواندم. کتاب فوق العاده ای بود و حاوی بینش زیادی بود. با این حال، تا سال 2014 تعدادی از مشکلات عملکرد قابل توجه شناسایی شده در این کتاب با بهروزرسانیهای موتور مرورگر حل شده یا به میزان قابل توجهی کاهش یافته بود، و ما توانستیم تلاش بیشتری را روی نوشتن کد قابل نگهداری متمرکز کنیم.
اگر میخواهید در لبه بهینهسازی عملکرد بمانید، برای تغییر و اعتبارسنجی منظم آماده باشید.
درس هایی از گذشته
در حین تحقیق در مورد این موضوع، به موضوعی از لیست پستی هسته لینوکس مربوط به سال 2000 برخوردم که در مورد حذف برخی از بهینه سازی های بازگشایی حلقه بود که در نهایت عملکرد برنامه را بهبود بخشید. شامل این نکته هنوز هم مرتبط بود (تاکید از من است):
نکته اصلی این است که مفروضات شهودی ما در مورد اینکه چه چیزی سریع است و چه چیزی نیست اغلب می تواند اشتباه باشد. به خصوص با توجه به تغییرات CPU در طول چند سال گذشته.- تئودور تسو
نتیجه
مواقعی وجود دارد که ممکن است لازم باشد عملکرد را از یک حلقه خارج کنید، و اگر به اندازه کافی موارد را پردازش می کنید، این می تواند یکی از راه های انجام این کار باشد. دانستن این نوع بهینهسازیها خوب است، اما برای بیشتر کارها، شما به آن نیاز ندارید™.
با این حال، امیدوارم که از سر و صدای من لذت برده باشید، و شاید در آینده حافظه شما در مورد ملاحظات بهینه سازی عملکرد ضعیف شود.
با تشکر برای خواندن!
جاوا اسکریپت میتواند از سختافزاری که روی آن اجرا میشود بسیار حذف شده باشد، اما تفکر در سطح پایین همچنان میتواند در موارد محدود مفید باشد.
پست اخیر Kafeel Ahmad در مورد بهینه سازی حلقه، تعدادی از تکنیک های بهبود عملکرد حلقه را شرح داده است. آن مقاله مرا در مورد موضوع فکر کرد.
بهینه سازی زودرس
فقط برای از بین بردن این موضوع، این تکنیکی است که تعداد کمی از آنها نیاز به در نظر گرفتن در توسعه وب دارند. همچنین، تمرکز روی بهینهسازی خیلی زود میتواند نوشتن کد را سختتر و نگهداری آن را سختتر کند. نگاهی به تکنیکهای سطح پایین میتواند به ما بینشی نسبت به ابزارها و کار به طور کلی بدهد، حتی اگر نتوانیم آن دانش را مستقیماً به کار ببریم.
Loop Unrolling چیست؟
باز کردن حلقه اساساً منطق درون یک حلقه را کپی می کند، بنابراین شما چندین عملیات را در طول هر یک انجام می دهید، خوب، حلقه. در موارد خاص، ساخت کد در حلقه طولانی تر می تواند آن را بسازد سریعتر.
با انجام عمدی برخی عملیات در گروه ها به جای یک به یک، کامپیوتر ممکن است بتواند کارآمدتر کار کند.
مثال باز کردن
بیایید یک مثال بسیار ساده بگیریم: جمع کردن مقادیر در یک آرایه.
// 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;
};
این ممکن است در ابتدا بسیار عجیب به نظر برسد. ما متغیرهای بیشتری را مدیریت می کنیم و عملیات اضافی را انجام می دهیم که در مثال ساده اتفاق نمی افتد. چگونه می تواند این سریع تر باشد؟!
اندازه گیری تفاوت
من چند مقایسه را روی انواع مختلف انجام دادم data
اندازهها و اجراهای متعدد، و همچنین آزمایشهای متوالی یا میانلایهای. این parallelSum
عملکرد متفاوت بود، اما تقریباً همیشه بهتر بود، به جز برخی از نتایج عجیب و غریب برای اندازههای داده بسیار کوچک. من این را با استفاده از RunJS آزمایش کردم که بر روی موتور V8 کروم ساخته شده است.
اندازه داده های مختلف داده است خیلی تقریبی این نتایج:
- کم اهمیت (
- متوسط (100-100k): معمولاً 20-80٪ سریعتر است
- بزرگ (> 1M): به طور مداوم دو برابر سریعتر
سپس یک JSPerf با 1 میلیون رکورد ایجاد کردم تا در مرورگرهای مختلف امتحان کنم. خودت آن را امتحان کن!
کروم اجرا شد parallelSum
دو برابر سریعتر از simpleSum
همانطور که از تست RunJS انتظار می رود.
سافاری هم از نظر درصد و هم از نظر عملیات در ثانیه تقریباً مشابه کروم بود.
فایرفاکس روی همان سیستم تقریباً همین کار را انجام داد simpleSum
ولی parallelSum
فقط حدود 15 درصد سریعتر بود، نه دو برابر.
این تنوع مرا به دنبال اطلاعات بیشتر فرستاد. در حالی که هیچ چیز قطعی نیست، من یک نظر StackOverflow از سال 2016 پیدا کردم که در مورد برخی از مشکلات موتور JS با باز کردن حلقه بحث می کرد. این نگاه جالبی است به اینکه چگونه موتورها و بهینهسازیها میتوانند بر روی کد تأثیر بگذارند به روشهایی که ما انتظار نداریم.
تغییرات
من یک نسخه سوم را نیز امتحان کردم، که دو مقدار را در یک عملیات اضافه کرد تا ببینم آیا تفاوت محسوسی بین یک متغیر و دو وجود دارد یا خیر.
const parallelSum = (data) => {
let sum = 0
for(let i=0; i data.length; i += 2) {
sum += data[i] + data[i + 1];
}
return sum;
};
پاسخ کوتاه: خیر. دو نسخه «موازی» در محدوده خطای گزارش شده یکدیگر قرار داشتند.
بنابراین، چگونه کار می کند؟
در حالی که جاوا اسکریپت تک رشتهای است، مفسرها، کامپایلرها و سختافزار زیر میتوانند در صورت وجود شرایط خاص، بهینهسازیهایی را برای ما انجام دهند.
در مثال ساده، عملیات به مقدار نیاز دارد i
برای اینکه بدانید چه دادههایی باید واکشی شوند، و به آخرین مقدار نیاز دارد sum
برای به روز رسانی. از آنجایی که هر دوی اینها در هر حلقه تغییر می کند، کامپیوتر باید منتظر باشد تا حلقه کامل شود تا داده های بیشتری دریافت کند. در حالی که ممکن است برای ما واضح به نظر برسد که چه چیزی i += 1
این کار را انجام می دهد، کامپیوتر بیشتر می فهمد “مقدار تغییر خواهد کرد، بعداً بررسی کنید”، بنابراین در بهینه سازی مشکل دارد.
نسخه های موازی ما چندین ورودی داده را برای هر مقدار بارگذاری می کنند i
. ما هنوز به آن وابسته هستیم sum
برای هر حلقه، اما میتوانیم دو برابر بیشتر داده در هر چرخه بارگذاری و پردازش کنیم. اما این بدان معنا نیست که اجرا می شود دو برابر سریعتر.
شیرجه عمیق تر
برای درک اینکه چرا باز کردن حلقه کار می کند، ما به عملکرد سطح پایین یک کامپیوتر نگاه می کنیم. پردازنده هایی با معماری فوق اسکالر می توانند چندین خط لوله برای انجام عملیات همزمان داشته باشند. آنها می توانند از اجرای خارج از دستور پشتیبانی کنند تا عملیاتی که به یکدیگر وابسته نیستند در اسرع وقت انجام شوند. برای برخی از عملیات، SIMD می تواند یک عمل را روی چندین قطعه داده به طور همزمان انجام دهد. فراتر از آن، ما شروع به ورود به کش، واکشی داده ها و پیش بینی شاخه می کنیم…
اما این یک مقاله جاوا اسکریپت است! ما آنقدر عمیق نمی رویم. اگر می خواهید در مورد معماری پردازنده بیشتر بدانید، Anandtech دارای برخی از Deep Dives عالی است.
محدودیت ها و معایب
باز کردن حلقه جادویی نیست. محدودیتها و بازدههای رو به کاهشی وجود دارد که به دلیل اندازه برنامه یا داده، پیچیدگی عملیات، معماری رایانه و موارد دیگر ظاهر میشوند. اما ما فقط یک یا دو عملیات را آزمایش کردهایم و رایانههای مدرن اغلب از چهار رشته یا بیشتر پشتیبانی میکنند.
برای امتحان برخی افزایشهای بزرگتر، یک JSPerf دیگر با رکوردهای 1، 2، 4 و 10 ساختم و آن را روی Apple M1 Max MacBook Pro با macOS 14.5 Sonoma و یک رایانه AMD Ryzen 9 3950X با ویندوز 11 اجرا کردم.
ده رکورد در یک زمان 2.5-3.5 برابر سریعتر از حلقه پایه بود، اما فقط 12-15٪ سریعتر از پردازش چهار رکورد در مک بود. در رایانه شخصی ما هنوز شاهد بهبود 2 برابری بین یک تا دو رکورد بودیم، اما ده رکورد فقط 2٪ سریعتر از چهار رکورد بود، که من برای یک پردازنده 16 هسته ای پیش بینی نمی کردم.
پلتفرم ها و به روز رسانی ها
این نتایج متفاوت به ما یادآوری می کند که مراقب بهینه سازی باشیم. بهینه سازی برای رایانه شما می تواند تجربه بدتری را در سخت افزارهای کم توان یا فقط متفاوت ایجاد کند. مشکلات مربوط به عملکرد یا عملکرد سخت افزارهای قدیمی یا سطح ابتدایی یک مشکل رایج زمانی است که توسعه دهندگان بر روی ماشین های سریع و قدرتمند کار می کنند، و این چیزی است که من چندین بار در حرفه خود وظیفه انجام آن را داشته ام.
برای برخی از مقیاسهای عملکرد، یک Chromebook سطح پایه فعلی موجود از HP دارای پردازنده Intel Celeron N4120 است. این تقریباً معادل Core i5-4250U MacBook Air 2013 من است. فقط دارد یک نهم عملکرد M1 Max در یک معیار مصنوعی. در مک بوک ایر 2013 که آخرین نسخه کروم را اجرا می کند، تابع 4 رکورد سریعتر از رکورد 10 بود، اما هنوز فقط 60٪ سریعتر از عملکرد تک رکورد بود!
مرورگرها و استانداردها نیز دائما در حال تغییر هستند. یک بهروزرسانی معمول مرورگر یا معماری متفاوت پردازنده میتواند کد بهینهسازی را ایجاد کند آرام تر از یک حلقه معمولی هنگامی که متوجه شدید که عمیقاً در حال بهینه سازی هستید، ممکن است نیاز داشته باشید که بهینه سازی خود را به مصرف کنندگان خود مرتبط و مرتبط باشد مرتبط باقی می ماند.
این کتاب من را به یاد کتاب جاوا اسکریپت با کارایی بالا اثر نیکلاس زکاس می اندازد که در سال 2012 خواندم. کتاب فوق العاده ای بود و حاوی بینش زیادی بود. با این حال، تا سال 2014 تعدادی از مشکلات عملکرد قابل توجه شناسایی شده در این کتاب با بهروزرسانیهای موتور مرورگر حل شده یا به میزان قابل توجهی کاهش یافته بود، و ما توانستیم تلاش بیشتری را روی نوشتن کد قابل نگهداری متمرکز کنیم.
اگر میخواهید در لبه بهینهسازی عملکرد بمانید، برای تغییر و اعتبارسنجی منظم آماده باشید.
درس هایی از گذشته
در حین تحقیق در مورد این موضوع، به موضوعی از لیست پستی هسته لینوکس مربوط به سال 2000 برخوردم که در مورد حذف برخی از بهینه سازی های بازگشایی حلقه بود که در نهایت عملکرد برنامه را بهبود بخشید. شامل این نکته هنوز هم مرتبط بود (تاکید از من است):
نکته اصلی این است که مفروضات شهودی ما در مورد اینکه چه چیزی سریع است و چه چیزی نیست اغلب می تواند اشتباه باشد. به خصوص با توجه به تغییرات CPU در طول چند سال گذشته.
– تئودور تسو
نتیجه
مواقعی وجود دارد که ممکن است لازم باشد عملکرد را از یک حلقه خارج کنید، و اگر به اندازه کافی موارد را پردازش می کنید، این می تواند یکی از راه های انجام این کار باشد. دانستن این نوع بهینهسازیها خوب است، اما برای بیشتر کارها، شما به آن نیاز ندارید™.
با این حال، امیدوارم که از سر و صدای من لذت برده باشید، و شاید در آینده حافظه شما در مورد ملاحظات بهینه سازی عملکرد ضعیف شود.
با تشکر برای خواندن!