چطوری ۴ بایت پدینگ اضافه، پاک کردن آرایه رو تو Go تا ۴۹ درصد سریعتر میکنه
خلاصهٔ کاملتر
نویسنده تو این پست، که بخشی از یه سری دربارهی ترفندهای بهینهسازیه، از یه کوئیرک عجیب تو زبان Go میگه: اگه بین فیلد اول یه struct و آرایهی داخلش ۴ بایت پدینگ اضافه کنی، تابع پاک کردن (reset) اون آرایه روی پردازندههای اینتل تا ۴۹ درصد سریعتر میشه. این نتیجه رو با بنچمارک کردن چند تا نسخه از یه struct با پدینگهای مختلف (۰ تا ۲۸ بایت، هر بار ۴ بایت اضافه) گرفته و یه الگوی زبرا دیده: هر پدینگی که مضرب ۸ باشه سریعتره، هر چیزی غیر از اون کندتره.
نویسنده رفته سراغ اسمبلی و دیده کل عملیات پاک کردن با یه دستور به اسم REP STOSQ انجام میشه. طبق مستندات بهینهسازی اینتل، وقتی بافر مقصد همتراز نباشه، کارایی memset میتونه تا ۲۰ درصد افت کنه. حدس نویسنده اینه که پردازنده یه مسیر بهینهی مخصوص داره که کل cache line رو بدون خوندنش مینویسه، ولی وقتی offset ۴ بایتی باعث بشه هر نوشتن ۸ بایتی از مرز ۶۴ بایتی cache line رد بشه، اون مسیر بهینه دیگه فعال نمیشه. این حدس رو با شمارش رویدادهای پردازنده هم تأیید کرده: حالت همتراز نشده باعث افزایش قابل توجه خواندنهای اضافه از کش سطح دو میشه.
نویسنده این آزمایش رو روی AMD هم تکرار کرده و همون الگو رو دیده، ولی خیلی ملایمتر (حدود ۹ درصد بهجای ۴۹ درصد). روی ARM اصلاً هیچ اختلافی مشاهده نشده. همچنین بین دو نسخه از دستورهای پاککردن حافظه (که یکی هر بار یک بایت و اون یکی هر بار ۸ بایت کپی میکنه) تفاوت معناداری پیدا نکرده، و Go خودش همیشه نسخهی ۸ بایتی رو انتخاب میکنه چون تضمین میده که کل کلمهی اشارهگر یکجا برای garbage collector قابل مشاهده باشه.
نویسنده همچنین راههای سریعتر SIMD رو هم امتحان کرده، ولی نسخههای استاندارد SIMD نتونستن از REP STOSQ جلو بزنن؛ فقط نسخههای «streaming» که مستقیم روی حافظه مینویسن گاهی سریعتر بودن، ولی نتیجهشون خیلی ناپایدار بود و به این بستگی داشت که کرنل یه بلوک پیوسته از حافظه بده یا پراکنده.
در نهایت نویسنده یه راهحل جایگزین هم پیشنهاد میده: بهجای پاک کردن واقعی آرایه، از یه برچسب نسل (generation tag) استفاده کن که با هر ریست فقط یه عدد رو زیاد میکنه، و دادههای قدیمی رو بهجای پاک کردن، بر اساس این برچسب نادیده میگیره. این روش فقط وقتی به کار میاد که دادهها تو موقعیتهای دلخواه آرایه نوشته بشن، نه فقط append بشن؛ اگه فقط append کنی، کافیه یه اندیس رو صفر کنی و اصلاً نیازی به پاک کردن نیست.
نکات کلیدی:
- افزودن ۴ بایت پدینگ بین یک فیلد و یک آرایهی uint32 تو یه struct میتونه پاک کردن اون آرایه رو روی اینتل تا ۴۹ درصد سریعتر کنه
- علتش اینه که دستور REP STOSQ فقط وقتی آدرس شروع مضرب ۸ بایته، از مسیر بهینهی نوشتن مستقیم روی cache line استفاده میکنه
- روی AMD این اثر خیلی ملایمتره (حدود ۹٪) و روی ARM اصلاً دیده نمیشه
- نسخههای SIMD معمولی نتونستن از REP STOSQ جلو بزنن؛ نسخههای streaming گاهی سریعتر بودن ولی نتیجهشون ناپایدار بود
- راهحل جایگزین برای پرهیز از پاک کردن مکرر، استفاده از generation tag روی هر عنصر آرایهست




