عملکرد نوع عدد صحیح – انجمن DEV

مشکل
C++ چندین نوع عدد صحیح را تعریف می کند: نسخه های امضا شده و بدون علامت char
، short
، int
، long
، و long long
، که معمولاً اندازه آنها از 8 بیت تا 64 بیت است. اگر نگرانی اصلی شما عملکرد است و باید یک عدد صحیح بین 0 تا 100 را نشان دهید، از کدام نوع استفاده کنید؟ در حالی که همه این انواع می توانند مقدار را ذخیره کنند، اینترنت توصیه های متناقضی ارائه می دهد: از کوچکترین نوع ممکن استفاده کنید، از نوع امضا شده استفاده کنید، از نوع بدون علامت استفاده کنید، یا همیشه استفاده کنید. int
.
راه حل
آزمایش من نشان می دهد که ترتیب اولویت بهینه انواع اعداد صحیح در AMD64، با فرض استفاده مناسب از [[assume()]]
، است
i8
u8
i16
u16
u32
i32
u64
i64
u128
i128
تست جیسون ترنر
جیسون ترنر اخیراً ویدیویی را منتشر کرده است که عملکرد GCC و Clang را در سطوح مختلف بهینه سازی بررسی می کند. یکی از چیزهای جالب او این است که Clang بهترین عملکرد را در یک برنامه بنچمارک هنگام استفاده از اعداد صحیح 32 بیتی به جای اعداد صحیح 8 یا 16 بیتی با فضای کارآمدتر ارائه می دهد. این من را شگفت زده کرد، بنابراین تصمیم گرفتم کمی عمیق تر به این موضوع نگاه کنم.
محدود شده::عدد صحیح
من یک کتابخانه به نام bounded:: کتابخانه عدد صحیح ایجاد کردم که به کاربران اجازه می دهد تا محدوده دقیق مقادیری را که یک نوع باید نمایش دهد را مشخص کنند (بنابراین bounded::integer<0, 100>
هر عدد صحیح بین 0 تا 100 را نشان می دهد). معیارهای قبلی من این را نشان داده است bounded::integer
اغلب با ارائه اطلاعات بیشتر به بهینه ساز و بهبود طرح داده، از اعداد صحیح داخلی بهتر عمل می کند.
از آنجا که من از این نوع برای تمام نیازهای اعداد صحیح خود استفاده می کنم، می توانم به راحتی انواع اعداد صحیح زیرین را در برنامه خود تغییر دهم و اثرات عملکرد را ببینم. تابعی که نوع اصلی را تعیین می کند به صورت زیر است:
template<auto minimum, auto maximum>
constexpr auto determine_type() {
if constexpr (range_fits_in_type<unsigned char>(minimum, maximum)) {
return type<unsigned char>;
} else if constexpr (range_fits_in_type<signed char>(minimum, maximum)) {
return type<signed char>;
} else if constexpr (range_fits_in_type<unsigned short>(minimum, maximum)) {
return type<unsigned short>;
} else if constexpr (range_fits_in_type<signed short>(minimum, maximum)) {
return type<signed short>;
} else if constexpr (range_fits_in_type<unsigned int>(minimum, maximum)) {
return type<unsigned int>;
} else if constexpr (range_fits_in_type<signed int>(minimum, maximum)) {
return type<signed int>;
} else if constexpr (range_fits_in_type<unsigned long>(minimum, maximum)) {
return type<unsigned long>;
} else if constexpr (range_fits_in_type<signed long>(minimum, maximum)) {
return type<signed long>;
} else if constexpr (range_fits_in_type<unsigned long long>(minimum, maximum)) {
return type<unsigned long long>;
} else if constexpr (range_fits_in_type<signed long long>(minimum, maximum)) {
return type<signed long long>;
#if defined NUMERIC_TRAITS_HAS_INT128
} else if constexpr (range_fits_in_type<numeric_traits::uint128_t>(minimum, maximum)) {
return type<numeric_traits::uint128_t>;
} else if constexpr (range_fits_in_type<numeric_traits::int128_t>(minimum, maximum)) {
return type<numeric_traits::int128_t>;
#endif
} else {
static_assert(false, "Bounds cannot fit in any type.");
}
}
این پیادهسازی اصلی من بود که ترتیب انواعی را که امتحان کردم نشان میدهد — اساساً از کوچکترین نوع داخلی استفاده میکنم و انواع بدون علامت را به انواع امضا شده ترجیح میدهم.
سخت افزار
تمام تستها روی یک AMD Ryzen 9 3950x غیرفعال بود.
کامپایلر
کد من در برابر تنه Clang ساخته شده در 9919295c با -O3
برای سطح بهینه سازی
تست من
برنامه ای که من آزمایش کردم وضعیت بازی ویدیویی را به روشی مشابه هوش مصنوعی شطرنج سنتی ارزیابی می کند. من یک حالت بازی نمونه تنظیم کردم و اندازهگیری کردم که چقدر طول میکشد تا با نمایشهای اعداد صحیح مختلف تجزیه و تحلیل شود.
برای آزمایش اینکه آیا نمایشهای کوچکتر عملکرد را بهبود میبخشند، کد را تغییر دادم تا نوع زیربنایی را تعیین کنم تا با 8 بیت شروع شود (پیشفرض، با کدی که نشان دادم مطابقت دارد)، سپس 16، 32 و در نهایت 64 بیت. برای آزمایش اعداد صحیح علامت دار در مقابل بدون علامت، هر جفت اعداد صحیح را نیز معکوس کردم تا در صورتی که عدد صحیح در هر یک از آنها قرار گیرد، مقادیر امضا شده بر بدون علامت ترجیح داده می شوند.
نتایج به صورت قالب بندی شده اند [bits in smallest integer representation (with a u prefix if unsigned types are preferred, and an i prefix if signed types are preferred)]: [time in seconds]
. به عنوان مثال، “i32” به معنای استفاده از اولین نوع است که می تواند مقدار را نشان دهد و انواع را به ترتیب مرور می کند i32
، u32
، i64
، u64
، i128
، u128
.
i8: 53.44
u8: 54.61
i16: 54.26
u16: 54.62
i32: 58.73
u32: 55.68
i64: 66.81
u64: 63.35
این نتایج حاصل از یک اجرا است. با این حال، اعداد عملکرد من پایدار هستند زیرا این یک برنامه به شدت بهینه شده است. به عنوان مثال، اجرای چندین بار بنچمارک u8 و i8 باعث می شود:
u8: 54.61، 54.40، 54.44، 54.17
i8: 53.44، 53.48، 53.51، 53.35
اجراهای u8 دارای ضریب تغییرات 0.33٪ (181 میلی ثانیه انحراف استاندارد)، و اجراهای i8 دارای ضریب تغییرات 0.13٪ (انحراف استاندارد 70 میلی ثانیه) هستند.
تفاوت بین نمادهای امضا شده و بدون امضا من را شگفت زده کرد. در bounded::integer
کتابخانه، دسترسی به یک مقدار شامل [[assume(minimum <= m_value and m_value <= maximum)]];
، که در تئوری باید به بهینهساز همه چیزهایی را که باید بداند برای حذف همه تفاوتهای علامتدار در مقابل بدون علامت میگوید. همچنین جالب است که حداقل در درخواست من، هیچ برنده ثابتی در مقابل بدون امضا وجود ندارد. این بدان معناست که بهینه ساز clang همیشه قادر به استنباط چیزهای قابل اثبات در مورد برنامه من نیست. برای یافتن علت اصلی این موضوع را بیشتر بررسی خواهم کرد.
بر اساس این نتایج، من سفارش جدیدی را آزمایش کردم که انواع کوچک امضا شده اما انواع بزرگ بدون علامت را ترجیح می دهد. این ترتیب اولویت تنظیم شده است i8
، u8
، i16
، u16
، u32
، i32
، u64
، i64
. این ترتیب بهترین نتایج 52.87 ثانیه را می دهد. همچنین میتوان فکر کرد که عملکرد را میتوان با حذف کامل u8 بهبود بخشید، اما به نظر میرسد که در واقع کمی سرعت آن را به 53.30 ثانیه کاهش میدهد.
مقایسه با نتایج جیسون
محک زدن برنامه من نتایج جیسون در مورد “اندازه صحیح بزرگتر == عملکرد سریعتر در Clang” را بازتولید نکرد. با توجه به بهبود عملکرد غول پیکر عجیب و غریب با یک نوع عدد صحیح خاص و یک کامپایلر خاص، من وسوسه می شوم که بگویم این چیزی عجیب در مورد معیار او است. توجه داشته باشید که معیار من نیز کمی متفاوت از جیسون است، زیرا شامل مفروضاتی است که به کامپایلر میگوید بدون توجه به نمایش شی، مقدار در محدوده خاصی قرار میگیرد.
با این حال، نکته اصلی جیسون این است که شما باید چیزهایی را برای خودتان آزمایش کنید تا بفهمید چه چیزی برای استفاده شما سریعتر است، که مخالفت با آن سخت است.