لینک کوتاه مطلب : https://hsgar.com/?p=4523

پیاده سازی Windows malloc یک Trash Fire است

من در حال حاضر یک زبان آزمایشی را به ویندوز منتقل می کنم. این زبان آزمایشی در C++ با LLVM ساخته شده است و به شدت به پسوندهای GCC مانند VLA ها و عبارات بیانیه مرکب، که اساساً ساختن با MSVC را غیرممکن کرد (اگرچه ایده واقعاً وحشتناکی دارم که ممکن است بعداً تلاش کنم). خوشبختانه اکنون می توانید با Clang چیزهایی را روی ویندوز بسازید که بسیاری از مشکلات را حل می کند. با این حال، clang-cl به سادگی کد را کامپایل می کند – همچنان از سربرگ های Microsoft C++ و پیوندهایی به زمان اجرا Microsoft C ++ استفاده می کند. این چیز خوبی است، زیرا حداکثر سازگاری را با API های win32 و سایر فایل های اجرایی ویندوز تضمین می کند.

متأسفانه به این معنی است که شما ویندوز را دریافت می کنید malloc() پیاده سازی از MSVCRT (به طور خاص، به طور ایستا با CRT ارسال شده با ویژوال استودیو ارتباط برقرار می کند)، که احتمالاً یکی از بدترین انبوه زباله های پوسیده است که تا به حال در تاریخ C گردآوری شده است. من یاد گرفتم که چگونه برنامه نویسی کنم، مانند بسیاری از افراد، توسط در حال انجام توسعه دهنده بازی مستقل مانند بسیاری از مردم، من هرگز یک بازی را منتشر نکردم، اما یک سری کد نوشتم که اکنون در مخازن گمشده GitHub فراموش شده است. به من آموختند که تخصیص حافظه به معنای احضار خود مرگ است تا عملکرد شما را خراب کند. یک تماس به malloc() در طول هر فریم به احتمال زیاد بازی شما غیر قابل پخش می شود. هر نوع تخصیصی که لازم بود با هر نظمی اتفاق بیفتد، نیاز به نوشتن یک تخصیص دهنده سفارشی و هدفمند دارد، معمولاً یا یک تخصیص دهنده بلوک با اندازه ثابت با استفاده از یک لیست آزاد یا یک تخصیص حریص که پس از پایان سطح آزاد می شود. حتی می‌توان با استفاده از ذخیره‌سازی thread-local، بهینه‌سازی بیشتری انجام داد تا تخصیص‌دهنده‌های رشته خاص را بدون نیاز به اتلاف زمان برای همزمانی حفظ کند.

به نظر می رسد که در لینوکس به هیچ کدام از اینها نیاز ندارید. شما اساسا می توانید malloc() هر آنچه که می خواهید (در حد منطق) و به طرز شگفت آوری سریع خواهد بود.

LLVM برای لینوکس ساخته شده است – یا بهتر است بگوییم، برای کارکردن برای Mac OSX ساخته شده است، که سازگار با POSIX است و اگر نگاه کنید مانند یک سیستم لینوکس به نظر می رسد. بیشتر بهینه‌سازی‌ها به گونه‌ای طراحی شده‌اند که آن را در مک یا لینوکس سریع کنند. از آنجایی که این یک کامپایلر است، تخصیص های بسیار کوچکی را انجام می دهد، زیرا اساساً جریان کنترل را به عنوان یک نمودار غول پیکر نشان می دهد. من در واقع فکر می کردم که از یک تخصیص دهنده سفارشی برای تخصیص این گره های کوچک استفاده می کند، زیرا این کاری است که من انجام می دادم، اما در واقعیت، فقط تماس می گیرد new همه جا و به لینوکس اجازه می دهد malloc() اجرا با آن برخورد کنید. دلیل اهمیت من این است که این زبان آزمایشی که روی آن کار می‌کنم باید کتابخانه اصلی خود را در هنگام راه‌اندازی JIT کند – تقریباً طول می‌کشد 1.1 ثانیه برای انجام این کار در لینوکس، و 31 ثانیه در ویندوز

در ابتدا فکر کردم این ناکارآمدی ناشی از استفاده از این زبان تجربی است std::unordered_map در همه جا، از آنجایی که این یک تکه حافظه جدید را برای هر آیتم اختصاص می دهد تا ثبات تکرار کننده را تضمین کند، و به دلیل ناکارآمدی باورنکردنی در مقایسه با هر پیاده سازی هش دیگر به خوبی شناخته شده است. من این را با گوگل جایگزین کردم آبسیل flat_hash_map پیاده سازیو به سرعت قابل توجه 2 برابری در ویندوز دست یافت و زمان راه اندازی را به 16 ثانیه کاهش داد. خیلی خوب و مطابق با چیزی که انتظار داشتم. آیا می توانید حدس بزنید که افزایش سرعت لینوکس مربوطه چقدر بوده است؟

هیچ چی.

به معنای واقعی کلمه. لعنتی هیچ چی.

ما این را هم در نمونه WSL من در ویندوز و هم در دسکتاپ NixOS بومی با نتایج یکسان آزمایش کردیم. با تصمیم به ادامه کار، ناحیه ناکارآمد دیگری از کامپایلر پیدا کردم که بیهوده یک بردار کامل را برای پشتیبانی از انواع bignum اختصاص می‌داد، حتی اگر هیچ یک از تست‌ها هرگز از این استفاده نکردند، بنابراین بردار در نهایت فقط یک عدد صحیح را نگه داشت. من آن را تغییر دادم تا اگر فقط 1 عنصر وجود داشت، از بردار رد شوم. با این کار 2 ثانیه دیگر از ویندوز قطع شد. به نظر می رسد که یا در لینوکس کاری انجام نمی دهد، یا به نوعی آن را کمی کندتر کرده است. بدتر از آن، بر اساس تجزیه و تحلیل پروفایلر من، چیزهای خارج از LLVM برای بهینه سازی تمام می شد – 14 ثانیه باقی مانده از زمان راه اندازی بیشتر LLVM بود.

بسیار خوب، لعنت به آن، این به وضوح یک مشکل تخصیص دهنده است، و اتفاقاً، برخی از توسعه دهندگان بازی شروع به استفاده از LLVM برای… به دلایلی، و البته، اکثر بازی ها روی ویندوز کار می کنند، بنابراین آنها به LLVM برای سریع بودن روی ویندوز نیاز داشتند. معرفی کردند آ بسیار راه ژانکی از جایگزینی malloc() در LLVM، که به نوعی مجبور شدم آن را به صورت سفارشی هک کنم vcpkg چنگال تا با مدیریت وابستگی من کار کند، اما به نظر می رسید که کار کند!

جز اینکه باعث خرابی LLVM شد.

معلوم می شود که اگر جایگزین کنید malloc() با هر کدام rpmalloc یا mimalloc، هسته ویندوز انجام می دهد گاهی هنگامی که در داخل هستید، یک دستورالعمل شکست اشکال زدایی را راه اندازی کنید std::recursive_mutex، اما فقط در صورتی که کد JITینگ دارید. این اتفاق می افتد حتی اگر threading را در LLVM غیرفعال کنید. در ابتدا فکر کردم که این نوعی عدم تطابق ABI است (از آنجا که این قبلا اتفاق افتاده است)، اما هیچ مقداری از تنظیمات برای مطابقت دادن، مشکل را برطرف نکرد. من با یکی از دوستان شخصی‌ام که اتفاقاً در اپل در LLVM کار می‌کند صحبت کردم، و آنها مشکوک هستند که این یک بررسی سلامت هسته است که برای گرفتن بن‌بست‌های احتمالی در نظر گرفته شده است، شاید به این دلیل که بخش مهم دوباره وارد نشده است و چیزی خراب شده است. با این حال، این در داخل یک بود std::recursive_mutex پیاده سازی، بنابراین… بنا به تعریف باید دوباره وارد شود. ممکن است به سادگی یک خطای شرط مسابقه یا پیاده سازی در جایی در LLVM وجود داشته باشد، اما من واقعاً حوصله رفع اشکال چند رشته ای بسیار محرمانه را در LLVM ندارم.

بنابراین، در عوض، با جایگزین کردن هک کارآمد، بدترین هک تمام عمرم را انجام دادم std::recursive_mutex پیاده سازی که از بخش های حیاتی با یک قفل چرخشی مجدد بسیار ناکارآمد استفاده می کرد.

این در واقع کار کرد و فوراً زمان راه اندازی ویندوز را تقریباً افزایش داد 1.4 ثانیه، اکنون در محدوده زمان شروع لینوکس است. را تخصیص دهنده ویندوز لعنتی منشا تمام مشکلات عملکرد من بود تمام زمان. آگانیا، که سپاس بی پایان من را برای به عهده گرفتن این وظیفه دارد فاجعه غرق قطار که LLD بود، اتفاقاً همان کسی است که روش Patching Allocator LLVM را معرفی کرد. متأسفانه، به نظر می‌رسد هر چیزی که از LLVM استفاده می‌کردند شامل کد JITing نمی‌شد، یا ممکن است از روش دیگری استفاده کرده باشند، زیرا به نظر می‌رسد هیچ‌کس به جز من با این مشکل مواجه نشده است.

بهترین بخش در مورد کل این شکست آن mimalloc است توسط مایکروسافت توسعه داده شد برای خدمات مایکروسافت به دلیل سرعت MSVCRT خود مایکروسافت malloc() بود. بنابراین در تمام این مدت سعی کردم مایکروسافت را جایگزین کنم malloc() با دیگر مایکروسافت malloc() چون مایکروسافت malloc() برای مایکروسافت خیلی کند بود. به دلایلی دیوانه‌وار، mimalloc در MSVCRT ارسال نمی‌شود، حتی به‌عنوان چیزی که باید در آن شرکت کنید (در صورت وجود نگرانی در مورد سازگاری با عقب). دستورالعمل های مایکروسافت ارائه می کند operator new به جای اینکه در واقع این را با آن ادغام کند، بیش از حد بارگذاری می کند کامپایلر خودشون با وجود مرتبه ای سریعتر در سناریوهای تخصیص کوچک سریع!

بنابراین، در این مرحله، ما سه چیز یاد گرفتیم: تخصیص دهنده ویندوز یک آتش سوزی کامل زباله است، مایکروسافت جایگزینی برای تخصیص دهنده خود اختراع کرد اما از ارائه آن به عنوان جایگزین خودداری کرد، و به وضوح نوعی شرایط مسابقه در JIT LLVM وجود دارد. موارد لبه خاصی مربوط به استفاده از mutexes در ویندوز است. اگر کسی می‌خواهد سعی کند این را بازتولید کند و یک باگ مناسب را در LLVM ثبت کند، ادامه دهید، اما چند روز طول می‌کشد تا نمونه‌ای حداقلی برای این کار تهیه شود و صادقانه بگویم که در حال حاضر کارهای بهتری برای انجام دادن دارم. وقتی threading غیرفعال است، spinlock ناکارآمد اهمیتی ندارد، زیرا هیچ مناقشه‌ای ندارد، و به نظر نمی‌رسد فعال بودن Threading، به هر دلیلی، باعث می‌شود که مورد استفاده من سریع‌تر پیش برود.

همه چیز خراب است و من خیلی خسته تر از آن هستم که بتوانم کاری انجام دهم، جز اینکه در بالای این انبوه کارت هایی که تمدن مدرن را روی آنها ساخته ایم، یک هک زشت دیگر انجام دهم.

لینک منبع

ارسال یک پاسخ

آدرس ایمیل شما منتشر نخواهد شد.