محل حافظه و عملکرد

اگرچه حافظه اغلب به عنوان یک مخزن ذخیره‌سازی واحد و یکنواخت در نظر گرفته می‌شود، اما سازماندهی فیزیکی آن و نحوه دسترسی CPU به آن تأثیر عمیقی بر عملکرد برنامه دارد. درک موقعیت مکانی حافظه، کلید نوشتن کد با کارایی بالا است که از سلسله مراتب حافظه پنهان CPU به طور کارآمد استفاده می‌کند.

سلسله مراتب حافظه نهان CPU

یک پردازنده مرکزی موبایل مدرن بسیار سریع‌تر از رم اصلی سیستم (DRAM) است. برای پر کردن این شکاف عملکرد، پردازنده‌ها از چندین سطح حافظه کوچک و بسیار سریع به نام حافظه پنهان (cache) استفاده می‌کنند.

  • حافظه نهان L1 (سطح 1) : کوچکترین و سریعترین (حدود 1 نانوثانیه). در یک پردازنده 3 گیگاهرتزی، این مقدار حدود 3 سیکل ساعت است.
  • حافظه نهان سطح ۲ (L2 Cache) : بزرگتر و کمی کندتر (حدود ۳-۵ نانوثانیه یا حدود ۱۰-۱۵ سیکل).
  • حافظه نهان سطح ۳ (L3 Cache) : بزرگترین حافظه نهان (حدود ۱۰ تا ۲۰ نانوثانیه یا حدود ۳۰ تا ۶۰ سیکل).
  • حافظه اصلی (DRAM) : بزرگترین و کندترین (حدود ۱۰۰ نانوثانیه یا حدود ۳۰۰+ چرخه).

هرم تأخیر حافظه

زمینه‌سازی تأخیر: هزینه‌ی یک وقفه

برای درک تأثیر این اعداد، یک CPU سوپراسکالر مدرن را در نظر بگیرید که می‌تواند ۴ تا ۸ دستورالعمل را در هر سیکل کلاک اجرا کند.

اگر CPU تمام حافظه‌های نهان را از دست بدهد و مجبور باشد ۱۰۰ نانوثانیه (۳۰۰ سیکل) برای خواندن یک DRAM صبر کند:

  • تعداد سیکل‌های از دست رفته : حدود ۳۰۰ سیکل.
  • دستورالعمل‌های «هدر رفته» : بین ۱۲۰۰ تا ۲۴۰۰ دستورالعمل که اگر داده‌ها از قبل در یک رجیستر محلی یا حافظه نهان L1 بودند، می‌توانستند اجرا شوند.

وقتی کد شما محل حافظه ضعیفی دارد، پردازنده لزوماً مشغول محاسبات پیچیده ریاضی نیست؛ بلکه اغلب "متوقف می‌شود" و در حالی که منتظر زیرسیستم حافظه است، برای هزاران معادل دستورالعمل بیکار می‌ماند.

دستورالعمل در هر چرخه (IPC)

یک معیار کلیدی برای اندازه‌گیری این کارایی، تعداد دستورالعمل‌ها در هر چرخه (IPC) است. IPC نشان می‌دهد که CPU به طور متوسط ​​در هر چرخه ساعت چند دستورالعمل را با موفقیت "کنار می‌گذارد" (تکمیل می‌کند).

  • IPC بالا (مثلاً 3.0 - 5.0) : پردازنده با راندمان بالا کار می‌کند و احتمالاً بیشتر داده‌های خود را در حافظه‌های نهان یا رجیسترهای L1/L2 پیدا می‌کند.
  • IPC پایین (مثلاً کمتر از 0.5) : پردازنده به شدت در تنگنا قرار دارد. حتی اگر پردازنده در مانیتورهای سیستم در حالت "استفاده" 100٪ باشد، در واقع بیشتر آن صرف انتظار برای حافظه می‌شود - حالتی که به عنوان توقف حافظه شناخته می‌شود.

محل حافظه عامل اصلی است که تعیین می‌کند آیا یک حلقه داده‌محور با IPC بالا اجرا می‌شود یا به مجموعه‌ای از وقفه‌ها تبدیل می‌شود.

خطوط حافظه پنهان

پردازنده‌ها بایت‌های تکی را از حافظه بارگذاری نمی‌کنند. در عوض، بلوک‌هایی با اندازه ثابت به نام خطوط حافظه پنهان (cache lines ) را بارگذاری می‌کنند که معمولاً ۶۴ بایت هستند. وقتی به یک متغیر دسترسی پیدا می‌کنید، پردازنده کل قطعه ۶۴ بایتی حاوی آن را به حافظه پنهان (cache) منتقل می‌کند.

مکانیک خط کش

TLB (بافر کناری ترجمه)

اندروید از حافظه مجازی استفاده می‌کند. هر دسترسی به حافظه نیاز به ترجمه یک آدرس مجازی به یک آدرس فیزیکی دارد. TLB یک حافظه پنهان تخصصی است که ترجمه‌های اخیر را ذخیره می‌کند. یک خطای TLB مستلزم آن است که هسته، جداول صفحه را در حافظه اصلی پیمایش کند، که در مقایسه با خطای TLB، عملیاتی نسبتاً پرهزینه است.


مشخصات سخت‌افزاری: پیکسل ۱۰ پرو فولد

برای تمرین‌های زیر، ما از یک دستگاه سخت‌افزاری Pixel 10 Pro Fold استفاده کردیم. این دستگاه دارای سیستم روی چیپ Google Tensor G5 است.

بازجویی از سخت‌افزار

برای درک زیرسیستم حافظه، ابتدا پیکربندی CPU و پارامترهای حافظه پنهان را بررسی می‌کنیم.

# Check CPU architecture and core parts
adb shell cat /proc/cpuinfo | grep 'CPU part' | sort -u
# Output:
# CPU part  : 0xd8b
# CPU part  : 0xd8c
# CPU part  : 0xd90

# Check cache line size
adb shell getconf -a | grep CACHE_LINESIZE
# Output:
# LEVEL1_ICACHE_LINESIZE             64
# LEVEL1_DCACHE_LINESIZE             64

رمزگشایی قطعات CPU

مقادیر CPU part در /proc/cpuinfo شناسه‌های هگزادسیمال برای هسته‌های CPU ARM هستند. برای Laguna SoC موجود در Pixel 10 Pro Fold، این مقادیر به صورت زیر نگاشت می‌شوند:

  • 0xd8b : ARM Cortex-A520 (هسته‌های کارآمد)
  • 0xd90 : ARM Cortex-A720 (هسته‌های عملکردی)
  • 0xd8c : ARM Cortex-X4 (هسته اصلی)

این پیکربندی ۴+۳+۱ در SoCهای موبایل مدرن رایج است، جایی که خوشه‌های مختلف ممکن است اندازه حافظه پنهان و تأخیرهای متفاوتی داشته باشند.


انواع محلات

طراحی کارآمد نرم‌افزار به دو نوع اصلی از موقعیت مکانی متکی است:

  1. مکان مکانی : اگر به یک مکان حافظه دسترسی پیدا شود، احتمالاً به مکان‌های حافظه مجاور نیز به زودی دسترسی پیدا خواهد شد. پیمایش ترتیبی آرایه نمونه کلاسیک این مورد است. از آنجا که CPU کل یک خط حافظه پنهان را بارگذاری می‌کند، دسترسی به عنصر بعدی در یک آرایه اگر از قبل در خط حافظه پنهان باشد، تقریباً "رایگان" است.
  2. محلی بودن زمانی : اگر به مکانی از حافظه دسترسی پیدا شود، احتمالاً به زودی دوباره به همان مکان دسترسی پیدا خواهد شد. الگوریتم‌های خوب، داده‌ها را در حالی که هنوز در حافظه پنهان "داغ" هستند، دوباره استفاده می‌کنند.

تمرین عملی: اندازه‌گیری موقعیت مکانی با simpleperf

در این تمرین، ما simpleperf برای نظارت بر شمارنده‌های عملکرد سخت‌افزار در حین اجرای دو پیمایش مختلف از یک ماتریس ۲۵۶ مگابایتی استفاده خواهیم کرد.

  1. پیمایش سطر به سطر : به عناصر ماتریس به ترتیبی که در حافظه ذخیره شده‌اند دسترسی پیدا می‌کند. این روش با حافظه پنهان سازگار است و از موقعیت مکانی بهره می‌برد.
  2. پیمایش ستون به ستون : برای دسترسی به عناصر بر اساس ستون، در حافظه پرش می‌کند. این روش اغلب حافظه پنهان و TLB را از دست می‌دهد و باعث می‌شود CPU از کار بیفتد.

۱. با Simpleperf اجرا کنید

فایل باینری را وارد کنید، مطمئن شوید که قابل اجرا است و simpleperf stat برای اندازه‌گیری رویدادهای حافظه پنهان و TLB استفاده کنید. ما از پسوند :u برای اندازه‌گیری رویدادها در فضای کاربری استفاده می‌کنیم. این دستورات برای دسترسی به شمارنده‌های PMU سخت‌افزاری در اکثر دستگاه‌ها به adb root نیاز دارند.

adb root
adb shell "chmod +x /data/local/tmp/LocalityLab"

مشخصات ردیف-رشته تحصیلی:

adb shell "simpleperf stat -e cpu-cycles:u,instructions:u,cache-misses:u,L1-dcache-load-misses:u,dTLB-load-misses:u /data/local/tmp/LocalityLab row"

ستون مشخصات - رشته تحصیلی:

adb shell "simpleperf stat -e cpu-cycles:u,instructions:u,cache-misses:u,L1-dcache-load-misses:u,dTLB-load-misses:u /data/local/tmp/LocalityLab col"

۲. اندازه‌گیری‌های نمونه (پیکسل ۱۰ پرو فولد)

نتایج زیر بر روی یک دستگاه سخت‌افزاری Pixel 10 Pro Fold اندازه‌گیری شده است:

متریک ردیف-ماژور (دوستانه) ستون اصلی (غیردوستانه) تفاوت
زمان اجرا ۰.۸۳ ثانیه ۶۸.۳ ثانیه ۸۲ برابر کندتر
دستورالعمل‌ها ۵.۲۷ میلیارد ۱۰.۲۰ میلیارد ۱.۹ برابر بیشتر
چرخه‌های پردازنده ۱.۲۰ میلیارد ۶۲.۱۸ میلیارد ۵۲ برابر بیشتر
دستورالعمل در هر چرخه (IPC) ۴.۴۰ ۰.۱۶ ۲۷ برابر راندمان کمتر
حافظه نهان داده سطح ۱ (L1 Data Cache) دچار مشکل می‌شود ۲۱۰ میلیون ۳,۳۶۹ میلیون ۱۶ برابر بیشتر از دست رفته
خطاهای بارگذاری dTLB ۰.۱۳ میلیون ۲,۸۸۸ میلیون ۲۲۰۰۰ برابر بیشتر از دست رفته

۳. تحلیل نتایج

  • افت IPC : در تست row-major، پردازنده به IPC برابر با ۴.۴۰ دست می‌یابد که نشان می‌دهد چندین دستورالعمل را در هر سیکل به طور کارآمد اجرا می‌کند. در تست column-major، IPC به ۰.۱۶ کاهش می‌یابد. این بدان معناست که پردازنده ۹۶٪ مواقع متوقف می‌شود و منتظر رسیدن داده از DRAM است.
  • گلوگاه TLB : چشمگیرترین تفاوت در خطای بارگذاری dTLB است. دسترسی ترتیبی (ردیف اصلی) در همان صفحات حافظه باقی می‌ماند و در نتیجه خطای TLB بسیار کمی رخ می‌دهد. پرش از ستون‌ها (ستون اصلی) باعث می‌شود CPU دائماً به صفحات جدید مراجعه کند، TLB را تحت فشار قرار دهد و پیمایش‌های پرهزینه جدول صفحه را مجبور کند.
  • بهره‌وری حافظه پنهان : پیمایش ستون اصلی، ۱۶ برابر خطای حافظه پنهان سطح ۱ بیشتر ایجاد می‌کند و CPU را مجبور می‌کند دائماً داده‌ها را از سطح ۳ یا DRAM که بسیار کندتر است، دریافت کند.

مشاهده: اگرچه هر دو پیمایش عملیات منطقی یکسانی را روی داده‌های یکسان انجام دادند، پیمایش ستون اصلی بیش از ۸۰ برابر کندتر بود. این تفاوت فاحش کاملاً به دلیل نحوه تعامل الگوی دسترسی با واقعیت فیزیکی زیرسیستم حافظه CPU است.

تعقیب اشاره‌گر در ساختارهای داده جاوا و کاتلین

در حالی که معیار ماتریس دوبعدی، محلیت مکانی را در آرایه‌های بومی پیوسته نشان می‌دهد، اکثر کدهای برنامه‌های اندروید و چارچوب‌ها به زبان‌های جاوا و کاتلین نوشته شده‌اند. در زبان‌های مدیریت‌شده، متغیرهای شیء و عناصر مجموعه، اشیاء را به صورت درون‌خطی ذخیره نمی‌کنند؛ آن‌ها ارجاعات (اشاره‌گرها) به اشیاء تخصیص‌یافته به پشته را که در سراسر پشته ART پراکنده شده‌اند، ذخیره می‌کنند.

هزینه گراف‌های مرجع تو در تو

یک الگوی رایج در برنامه‌های اندروید و سرویس‌های سیستمی را در نظر بگیرید: پیمایش مجموعه‌های تو در تو مانند یک ArrayList از اشیاء وضعیت، که هر کدام شامل یک ArrayMap یا ArraySet از شنونده‌ها یا اتصالات هستند که هر کدام به یک رکورد وضعیت دیگر اشاره می‌کنند.

اگرچه ArrayList ، ArrayMap و ArraySet آرایه‌های داخلی Object[] خود را به صورت پیوسته ذخیره می‌کنند، اما هر عنصر در آن Object[] هنوز یک ارجاع heap است. ارجاع مجدد به یک زنجیره مانند process.services.valueAt(i).connections.valueAt(j).client نیاز به پنج بارگذاری حافظه وابسته متوالی دارد:

  1. services پشتیبان Object[] را بارگذاری کنید.
  2. هدر و فیلدهای شیء ServiceRecord را بارگذاری کنید.
  3. connections پشتیبان Object[] را بارگذاری کنید.
  4. شیء ConnectionRecord را بارگذاری کنید.
  5. فیلد ProcessRecord هدف را بارگذاری کنید.

از آنجا که آدرس حافظه هر بار بارگذاری به مقداری که توسط بار قبلی برگردانده شده بستگی دارد، موتور اجرای خارج از ترتیب CPU و پیش واکشی سخت‌افزاری نمی‌توانند آنها را با هم همپوشانی داشته باشند. اگر آن اشیاء در زمان‌های مختلف تخصیص داده شده باشند یا در حین جمع‌آوری زباله به مناطق مختلف منتقل شده باشند، هر گام خطر از دست رفتن حافظه نهان L1 یا L2 را به همراه دارد.

متغیرهای اولیه‌ی جعبه‌ای ( ArrayList<Integer> ، HashMap<Long, Boolean> ) و لامبداهای عمومی این سربار را تشدید می‌کنند: هر جستجوی عنصر برای خارج کردن مقدار از جعبه، به یک ارجاع اشاره‌گر اضافی نیاز دارد و فراخوانی‌های عمومی Consumer<T> استاب‌های بررسی نوع ( CheckCast ) زمان اجرا را وارد می‌کنند که فشار حافظه پنهان دستورالعمل ( L1-icache ) را اضافه می‌کنند.

تشخیص تعقیب اشاره‌گر با simpleperf

در بارهای کاری جاوا و کاتلین در دنیای واقعی (مانند نمودارهای مرجع فرآیند، سرویس و ارائه دهنده OomAdjuster در system_server )، تعقیب اشاره‌گر به ندرت IPC را تا 0.16 کاهش می‌دهد، مانند یک اسکن مصنوعی ستون-عمده 256 مگابایتی، زیرا بخشی از مجموعه کاری در حافظه نهان L2 یا L3 جای می‌گیرد. در عوض، به دنبال این امضای مشخصه در simpleperf باشید:

  • کاهش IPC (حدود ۰.۶ تا ۰.۹) : بسیار پایین‌تر از پهنای بازنشستگی سوپراسکالر پردازنده.
  • توقف‌های زیاد حافظه‌ی backend ( raw-stall-backend-mem ) : اغلب ۳۵ تا ۴۵ درصد از کل چرخه‌های CPU صرف انتظار برای پر شدن حافظه‌ی نهان (cache) داده می‌شود.
  • افزایش خطاهای L1-dcache-load-misses و بارگذاری کش سطح اول L1-icache-load-misses ) : نرخ بالای خطاهای کش داده به همراه خطاهای کش دستورالعمل‌ها، زمانی که حلقه‌های پیمایش داغ (hot transversal loops) از متدهای مجازی و stubهای عمومی لامبدا عبور می‌کنند.

شما می‌توانید این شمارنده‌ها را در یک فرآیند در حال اجرا با استفاده از simpleperf stat اندازه‌گیری کنید:

adb shell simpleperf stat \
  -e cpu-cycles:u,instructions:u,raw-stall-backend-mem:u,L1-dcache-load-misses:u,L1-icache-load-misses:u \
  -p $(pidof system_server) --duration 10

بهبود محلی بودن در کد مدیریت‌شده

  • مجموعه‌های جعبه‌ای را با آرایه‌های اولیه یا مجموعه‌های AndroidX جایگزین کنید : از IntArray ، LongArray ، SparseIntArray یا androidx.collection اولیه ( IntList ، LongLongMap ، ScatterMap ) برای حذف اشیاء پوششی و نگه داشتن مقادیر پیوسته در داخل یک تخصیص آرایه واحد استفاده کنید.
  • مسطح کردن مسیرهای پیمایش داغ : اگر یک حلقه داغ به طور مکرر سه یا چهار گام در یک گراف شیء طی کند تا یک پرچم بولی یا عدد صحیح را بخواند، آن حالت را در یک آرایه مسطح یا ماسک بیتی که توسط یک شناسه متراکم نمایه شده است، ذخیره یا ذخیره می‌کند.
  • از گرفتن یا استفاده از لامبداهای عمومی در حلقه‌های داخلی فشرده خودداری کنید : از حلقه‌های for با اندیس استاندارد روی لیست‌های RandomAccess به جای forEach یا زنجیره‌های تکرارکننده استفاده کنید تا از تخصیص تکرارکننده، ارسال مگامورفیک و سربار بررسی نوع در زمان اجرا جلوگیری شود.

← موضوعات | ↑ بالا | مقیدسازی سرویس →

،

اگرچه حافظه اغلب به عنوان یک مخزن ذخیره‌سازی واحد و یکنواخت در نظر گرفته می‌شود، اما سازماندهی فیزیکی آن و نحوه دسترسی CPU به آن تأثیر عمیقی بر عملکرد برنامه دارد. درک موقعیت مکانی حافظه، کلید نوشتن کد با کارایی بالا است که از سلسله مراتب حافظه پنهان CPU به طور کارآمد استفاده می‌کند.

سلسله مراتب حافظه نهان CPU

یک پردازنده مرکزی موبایل مدرن بسیار سریع‌تر از رم اصلی سیستم (DRAM) است. برای پر کردن این شکاف عملکرد، پردازنده‌ها از چندین سطح حافظه کوچک و بسیار سریع به نام حافظه پنهان (cache) استفاده می‌کنند.

  • حافظه نهان L1 (سطح 1) : کوچکترین و سریعترین (حدود 1 نانوثانیه). در یک پردازنده 3 گیگاهرتزی، این مقدار حدود 3 سیکل ساعت است.
  • حافظه نهان سطح ۲ (L2 Cache) : بزرگتر و کمی کندتر (حدود ۳-۵ نانوثانیه یا حدود ۱۰-۱۵ سیکل).
  • حافظه نهان سطح ۳ (L3 Cache) : بزرگترین حافظه نهان (حدود ۱۰ تا ۲۰ نانوثانیه یا حدود ۳۰ تا ۶۰ سیکل).
  • حافظه اصلی (DRAM) : بزرگترین و کندترین (حدود ۱۰۰ نانوثانیه یا حدود ۳۰۰+ چرخه).

هرم تأخیر حافظه

زمینه‌سازی تأخیر: هزینه‌ی یک وقفه

برای درک تأثیر این اعداد، یک CPU سوپراسکالر مدرن را در نظر بگیرید که می‌تواند ۴ تا ۸ دستورالعمل را در هر سیکل کلاک اجرا کند.

اگر CPU تمام حافظه‌های نهان را از دست بدهد و مجبور باشد ۱۰۰ نانوثانیه (۳۰۰ سیکل) برای خواندن یک DRAM صبر کند:

  • تعداد سیکل‌های از دست رفته : حدود ۳۰۰ سیکل.
  • دستورالعمل‌های «هدر رفته» : بین ۱۲۰۰ تا ۲۴۰۰ دستورالعمل که اگر داده‌ها از قبل در یک رجیستر محلی یا حافظه نهان L1 بودند، می‌توانستند اجرا شوند.

وقتی کد شما محل حافظه ضعیفی دارد، پردازنده لزوماً مشغول محاسبات پیچیده ریاضی نیست؛ بلکه اغلب "متوقف می‌شود" و در حالی که منتظر زیرسیستم حافظه است، برای هزاران معادل دستورالعمل بیکار می‌ماند.

دستورالعمل در هر چرخه (IPC)

یک معیار کلیدی برای اندازه‌گیری این کارایی، تعداد دستورالعمل‌ها در هر چرخه (IPC) است. IPC نشان می‌دهد که CPU به طور متوسط ​​در هر چرخه ساعت چند دستورالعمل را با موفقیت "کنار می‌گذارد" (تکمیل می‌کند).

  • IPC بالا (مثلاً 3.0 - 5.0) : پردازنده با راندمان بالا کار می‌کند و احتمالاً بیشتر داده‌های خود را در حافظه‌های نهان یا رجیسترهای L1/L2 پیدا می‌کند.
  • IPC پایین (مثلاً کمتر از 0.5) : پردازنده به شدت در تنگنا قرار دارد. حتی اگر پردازنده در مانیتورهای سیستم در حالت "استفاده" 100٪ باشد، در واقع بیشتر آن صرف انتظار برای حافظه می‌شود - حالتی که به عنوان توقف حافظه شناخته می‌شود.

محل حافظه عامل اصلی است که تعیین می‌کند آیا یک حلقه داده‌محور با IPC بالا اجرا می‌شود یا به مجموعه‌ای از وقفه‌ها تبدیل می‌شود.

خطوط حافظه پنهان

پردازنده‌ها بایت‌های تکی را از حافظه بارگذاری نمی‌کنند. در عوض، بلوک‌هایی با اندازه ثابت به نام خطوط حافظه پنهان (cache lines ) را بارگذاری می‌کنند که معمولاً ۶۴ بایت هستند. وقتی به یک متغیر دسترسی پیدا می‌کنید، پردازنده کل قطعه ۶۴ بایتی حاوی آن را به حافظه پنهان (cache) منتقل می‌کند.

مکانیک خط کش

TLB (بافر کناری ترجمه)

اندروید از حافظه مجازی استفاده می‌کند. هر دسترسی به حافظه نیاز به ترجمه یک آدرس مجازی به یک آدرس فیزیکی دارد. TLB یک حافظه پنهان تخصصی است که ترجمه‌های اخیر را ذخیره می‌کند. یک خطای TLB مستلزم آن است که هسته، جداول صفحه را در حافظه اصلی پیمایش کند، که در مقایسه با خطای TLB، عملیاتی نسبتاً پرهزینه است.


مشخصات سخت‌افزاری: پیکسل ۱۰ پرو فولد

برای تمرین‌های زیر، ما از یک دستگاه سخت‌افزاری Pixel 10 Pro Fold استفاده کردیم. این دستگاه دارای سیستم روی چیپ Google Tensor G5 است.

بازجویی از سخت‌افزار

برای درک زیرسیستم حافظه، ابتدا پیکربندی CPU و پارامترهای حافظه پنهان را بررسی می‌کنیم.

# Check CPU architecture and core parts
adb shell cat /proc/cpuinfo | grep 'CPU part' | sort -u
# Output:
# CPU part  : 0xd8b
# CPU part  : 0xd8c
# CPU part  : 0xd90

# Check cache line size
adb shell getconf -a | grep CACHE_LINESIZE
# Output:
# LEVEL1_ICACHE_LINESIZE             64
# LEVEL1_DCACHE_LINESIZE             64

رمزگشایی قطعات CPU

مقادیر CPU part در /proc/cpuinfo شناسه‌های هگزادسیمال برای هسته‌های CPU ARM هستند. برای Laguna SoC موجود در Pixel 10 Pro Fold، این مقادیر به صورت زیر نگاشت می‌شوند:

  • 0xd8b : ARM Cortex-A520 (هسته‌های کارآمد)
  • 0xd90 : ARM Cortex-A720 (هسته‌های عملکردی)
  • 0xd8c : ARM Cortex-X4 (هسته اصلی)

این پیکربندی ۴+۳+۱ در SoCهای موبایل مدرن رایج است، جایی که خوشه‌های مختلف ممکن است اندازه حافظه پنهان و تأخیرهای متفاوتی داشته باشند.


انواع محلات

طراحی کارآمد نرم‌افزار به دو نوع اصلی از موقعیت مکانی متکی است:

  1. مکان مکانی : اگر به یک مکان حافظه دسترسی پیدا شود، احتمالاً به مکان‌های حافظه مجاور نیز به زودی دسترسی پیدا خواهد شد. پیمایش ترتیبی آرایه نمونه کلاسیک این مورد است. از آنجا که CPU کل یک خط حافظه پنهان را بارگذاری می‌کند، دسترسی به عنصر بعدی در یک آرایه اگر از قبل در خط حافظه پنهان باشد، تقریباً "رایگان" است.
  2. محلی بودن زمانی : اگر به مکانی از حافظه دسترسی پیدا شود، احتمالاً به زودی دوباره به همان مکان دسترسی پیدا خواهد شد. الگوریتم‌های خوب، داده‌ها را در حالی که هنوز در حافظه پنهان "داغ" هستند، دوباره استفاده می‌کنند.

تمرین عملی: اندازه‌گیری موقعیت مکانی با simpleperf

در این تمرین، ما simpleperf برای نظارت بر شمارنده‌های عملکرد سخت‌افزار در حین اجرای دو پیمایش مختلف از یک ماتریس ۲۵۶ مگابایتی استفاده خواهیم کرد.

  1. پیمایش سطر به سطر : به عناصر ماتریس به ترتیبی که در حافظه ذخیره شده‌اند دسترسی پیدا می‌کند. این روش با حافظه پنهان سازگار است و از موقعیت مکانی بهره می‌برد.
  2. پیمایش ستون به ستون : برای دسترسی به عناصر بر اساس ستون، در حافظه پرش می‌کند. این روش اغلب حافظه پنهان و TLB را از دست می‌دهد و باعث می‌شود CPU از کار بیفتد.

۱. با Simpleperf اجرا کنید

فایل باینری را وارد کنید، مطمئن شوید که قابل اجرا است و simpleperf stat برای اندازه‌گیری رویدادهای حافظه پنهان و TLB استفاده کنید. ما از پسوند :u برای اندازه‌گیری رویدادها در فضای کاربری استفاده می‌کنیم. این دستورات برای دسترسی به شمارنده‌های PMU سخت‌افزاری در اکثر دستگاه‌ها به adb root نیاز دارند.

adb root
adb shell "chmod +x /data/local/tmp/LocalityLab"

مشخصات ردیف-رشته تحصیلی:

adb shell "simpleperf stat -e cpu-cycles:u,instructions:u,cache-misses:u,L1-dcache-load-misses:u,dTLB-load-misses:u /data/local/tmp/LocalityLab row"

ستون مشخصات - رشته تحصیلی:

adb shell "simpleperf stat -e cpu-cycles:u,instructions:u,cache-misses:u,L1-dcache-load-misses:u,dTLB-load-misses:u /data/local/tmp/LocalityLab col"

۲. اندازه‌گیری‌های نمونه (پیکسل ۱۰ پرو فولد)

نتایج زیر بر روی یک دستگاه سخت‌افزاری Pixel 10 Pro Fold اندازه‌گیری شده است:

متریک ردیف-ماژور (دوستانه) ستون اصلی (غیردوستانه) تفاوت
زمان اجرا ۰.۸۳ ثانیه ۶۸.۳ ثانیه ۸۲ برابر کندتر
دستورالعمل‌ها ۵.۲۷ میلیارد ۱۰.۲۰ میلیارد ۱.۹ برابر بیشتر
چرخه‌های پردازنده ۱.۲۰ میلیارد ۶۲.۱۸ میلیارد ۵۲ برابر بیشتر
دستورالعمل در هر چرخه (IPC) ۴.۴۰ ۰.۱۶ ۲۷ برابر راندمان کمتر
حافظه نهان داده سطح ۱ (L1 Data Cache) دچار مشکل می‌شود ۲۱۰ میلیون ۳,۳۶۹ میلیون ۱۶ برابر بیشتر از دست رفته
خطاهای بارگذاری dTLB ۰.۱۳ میلیون ۲,۸۸۸ میلیون ۲۲۰۰۰ برابر بیشتر از دست رفته

۳. تحلیل نتایج

  • افت IPC : در تست row-major، پردازنده به IPC برابر با ۴.۴۰ دست می‌یابد که نشان می‌دهد چندین دستورالعمل را در هر سیکل به طور کارآمد اجرا می‌کند. در تست column-major، IPC به ۰.۱۶ کاهش می‌یابد. این بدان معناست که پردازنده ۹۶٪ مواقع متوقف می‌شود و منتظر رسیدن داده از DRAM است.
  • گلوگاه TLB : چشمگیرترین تفاوت در خطای بارگذاری dTLB است. دسترسی ترتیبی (ردیف اصلی) در همان صفحات حافظه باقی می‌ماند و در نتیجه خطای TLB بسیار کمی رخ می‌دهد. پرش از ستون‌ها (ستون اصلی) باعث می‌شود CPU دائماً به صفحات جدید مراجعه کند، TLB را تحت فشار قرار دهد و پیمایش‌های پرهزینه جدول صفحه را مجبور کند.
  • بهره‌وری حافظه پنهان : پیمایش ستون اصلی، ۱۶ برابر خطای حافظه پنهان سطح ۱ بیشتر ایجاد می‌کند و CPU را مجبور می‌کند دائماً داده‌ها را از سطح ۳ یا DRAM که بسیار کندتر است، دریافت کند.

مشاهده: اگرچه هر دو پیمایش عملیات منطقی یکسانی را روی داده‌های یکسان انجام دادند، پیمایش ستون اصلی بیش از ۸۰ برابر کندتر بود. این تفاوت فاحش کاملاً به دلیل نحوه تعامل الگوی دسترسی با واقعیت فیزیکی زیرسیستم حافظه CPU است.

تعقیب اشاره‌گر در ساختارهای داده جاوا و کاتلین

در حالی که معیار ماتریس دوبعدی، محلیت مکانی را در آرایه‌های بومی پیوسته نشان می‌دهد، اکثر کدهای برنامه‌های اندروید و چارچوب‌ها به زبان‌های جاوا و کاتلین نوشته شده‌اند. در زبان‌های مدیریت‌شده، متغیرهای شیء و عناصر مجموعه، اشیاء را به صورت درون‌خطی ذخیره نمی‌کنند؛ آن‌ها ارجاعات (اشاره‌گرها) به اشیاء تخصیص‌یافته به پشته را که در سراسر پشته ART پراکنده شده‌اند، ذخیره می‌کنند.

هزینه گراف‌های مرجع تو در تو

یک الگوی رایج در برنامه‌های اندروید و سرویس‌های سیستمی را در نظر بگیرید: پیمایش مجموعه‌های تو در تو مانند یک ArrayList از اشیاء وضعیت، که هر کدام شامل یک ArrayMap یا ArraySet از شنونده‌ها یا اتصالات هستند که هر کدام به یک رکورد وضعیت دیگر اشاره می‌کنند.

اگرچه ArrayList ، ArrayMap و ArraySet آرایه‌های داخلی Object[] خود را به صورت پیوسته ذخیره می‌کنند، اما هر عنصر در آن Object[] هنوز یک ارجاع heap است. ارجاع مجدد به یک زنجیره مانند process.services.valueAt(i).connections.valueAt(j).client نیاز به پنج بارگذاری حافظه وابسته متوالی دارد:

  1. services پشتیبان Object[] را بارگذاری کنید.
  2. هدر و فیلدهای شیء ServiceRecord را بارگذاری کنید.
  3. connections پشتیبان Object[] را بارگذاری کنید.
  4. شیء ConnectionRecord را بارگذاری کنید.
  5. فیلد ProcessRecord هدف را بارگذاری کنید.

از آنجا که آدرس حافظه هر بار بارگذاری به مقداری که توسط بار قبلی برگردانده شده بستگی دارد، موتور اجرای خارج از ترتیب CPU و پیش واکشی سخت‌افزاری نمی‌توانند آنها را با هم همپوشانی داشته باشند. اگر آن اشیاء در زمان‌های مختلف تخصیص داده شده باشند یا در حین جمع‌آوری زباله به مناطق مختلف منتقل شده باشند، هر گام خطر از دست رفتن حافظه نهان L1 یا L2 را به همراه دارد.

متغیرهای اولیه‌ی جعبه‌ای ( ArrayList<Integer> ، HashMap<Long, Boolean> ) و لامبداهای عمومی این سربار را تشدید می‌کنند: هر جستجوی عنصر برای خارج کردن مقدار از جعبه، به یک ارجاع اشاره‌گر اضافی نیاز دارد و فراخوانی‌های عمومی Consumer<T> استاب‌های بررسی نوع ( CheckCast ) زمان اجرا را وارد می‌کنند که فشار حافظه پنهان دستورالعمل ( L1-icache ) را اضافه می‌کنند.

تشخیص تعقیب اشاره‌گر با simpleperf

در بارهای کاری جاوا و کاتلین در دنیای واقعی (مانند نمودارهای مرجع فرآیند، سرویس و ارائه دهنده OomAdjuster در system_server )، تعقیب اشاره‌گر به ندرت IPC را تا 0.16 کاهش می‌دهد، مانند یک اسکن مصنوعی ستون-عمده 256 مگابایتی، زیرا بخشی از مجموعه کاری در حافظه نهان L2 یا L3 جای می‌گیرد. در عوض، به دنبال این امضای مشخصه در simpleperf باشید:

  • کاهش IPC (حدود ۰.۶ تا ۰.۹) : بسیار پایین‌تر از پهنای بازنشستگی سوپراسکالر پردازنده.
  • توقف‌های زیاد حافظه‌ی backend ( raw-stall-backend-mem ) : اغلب ۳۵ تا ۴۵ درصد از کل چرخه‌های CPU صرف انتظار برای پر شدن حافظه‌ی نهان (cache) داده می‌شود.
  • افزایش خطاهای L1-dcache-load-misses و بارگذاری کش سطح اول L1-icache-load-misses ) : نرخ بالای خطاهای کش داده به همراه خطاهای کش دستورالعمل‌ها، زمانی که حلقه‌های پیمایش داغ (hot transversal loops) از متدهای مجازی و stubهای عمومی لامبدا عبور می‌کنند.

شما می‌توانید این شمارنده‌ها را در یک فرآیند در حال اجرا با استفاده از simpleperf stat اندازه‌گیری کنید:

adb shell simpleperf stat \
  -e cpu-cycles:u,instructions:u,raw-stall-backend-mem:u,L1-dcache-load-misses:u,L1-icache-load-misses:u \
  -p $(pidof system_server) --duration 10

بهبود محلی بودن در کد مدیریت‌شده

  • مجموعه‌های جعبه‌ای را با آرایه‌های اولیه یا مجموعه‌های AndroidX جایگزین کنید : از IntArray ، LongArray ، SparseIntArray یا androidx.collection اولیه ( IntList ، LongLongMap ، ScatterMap ) برای حذف اشیاء پوششی و نگه داشتن مقادیر پیوسته در داخل یک تخصیص آرایه واحد استفاده کنید.
  • مسطح کردن مسیرهای پیمایش داغ : اگر یک حلقه داغ به طور مکرر سه یا چهار گام در یک گراف شیء طی کند تا یک پرچم بولی یا عدد صحیح را بخواند، آن حالت را در یک آرایه مسطح یا ماسک بیتی که توسط یک شناسه متراکم نمایه شده است، ذخیره یا ذخیره می‌کند.
  • از گرفتن یا استفاده از لامبداهای عمومی در حلقه‌های داخلی فشرده خودداری کنید : از حلقه‌های for با اندیس استاندارد روی لیست‌های RandomAccess به جای forEach یا زنجیره‌های تکرارکننده استفاده کنید تا از تخصیص تکرارکننده، ارسال مگامورفیک و سربار بررسی نوع در زمان اجرا جلوگیری شود.

← موضوعات | ↑ بالا | مقیدسازی سرویس →