Как да ускорите кода си с помощта на кешове на процесора

Кешът на процесора намалява латентността на паметта при достъп до данни от основната системна памет. Разработчиците могат и трябва да се възползват от кеша на процесора, за да подобрят производителността на приложението.

Как работят кешовете на процесора

Съвременните процесори обикновено имат три нива на кеш памет, обозначени с L1, L2 и L3, което отразява реда, в който процесорът ги проверява. Процесорите често имат кеш данни, кеш инструкции (за код) и унифициран кеш (за каквото и да е). Достъпът до тези кешове е много по-бърз от достъпа до RAM: Обикновено L1 кешът е около 100 пъти по-бърз от RAM за достъп до данни, а L2 кешът е 25 пъти по-бърз от RAM за достъп до данни.

Когато вашият софтуер работи и трябва да изтегли данни или инструкции, първо се проверяват кешовете на процесора, след това по-бавната системна RAM и накрая много по-бавните дискови устройства. Ето защо искате да оптимизирате кода си, за да търсите първо това, което вероятно ще е необходимо от кеша на процесора.

Вашият код не може да посочи къде се намират инструкциите за данни и данните - компютърният хардуер прави това - така че не можете да принудите определени елементи в кеша на процесора. Но можете да оптимизирате кода си, за да извлечете размера на кеша L1, L2 или L3 във вашата система, като използвате Windows Management Instrumentation (WMI), за да оптимизирате кога приложението ви има достъп до кеша и по този начин неговата производителност.

Процесорите никога нямат достъп до байт по байт. Вместо това те четат паметта в кеш редове, които представляват парчета памет, обикновено 32, 64 или 128 байта.

Следният списък с кодове илюстрира как можете да извлечете размера на кеша на процесора L2 или L3 във вашата система:

публичен статичен uint GetCPUCacheSize (низ cacheType) {опитайте {използвайки (ManagementObject managementObject = нов ManagementObject ("Win32_Processor.DeviceID = 'CPU0'")) {return (uint) (managementObject [cacheType]); }} catch {return 0; }} static void Main (string [] args) {uint L2CacheSize = GetCPUCacheSize ("L2CacheSize"); uint L3CacheSize = GetCPUCacheSize ("L3CacheSize"); Console.WriteLine ("L2CacheSize:" + L2CacheSize.ToString ()); Console.WriteLine ("L3CacheSize:" + L3CacheSize.ToString ()); Console.Read (); }

Microsoft разполага с допълнителна документация за класа WMI на Win32_Processor.

Програмиране за изпълнение: Примерен код

Когато имате обекти в стека, няма никакви събиране на боклук над главата. Ако използвате обекти, базирани на купчина, винаги има разходи за събиране на боклук от поколения за събиране или преместване на обекти в купчината или уплътняване на паметта на купчината. Един добър начин да се избегне натрупването на боклук е използването на структури вместо класове.

Кешовете работят най-добре, ако използвате последователна структура от данни, като масив. Последователното подреждане позволява на процесора да чете напред и също да чете спекулативно в очакване на това, което вероятно ще бъде поискано следващото. По този начин алгоритъмът, който осъществява последователен достъп до паметта, винаги е бърз.

Ако имате достъп до паметта в произволен ред, процесорът се нуждае от нови кеш линии всеки път, когато имате достъп до паметта. Това намалява производителността.

Следният кодов фрагмент реализира проста програма, която илюстрира предимствата от използването на структура над клас:

 struct RectangleStruct {public int width; обществена височина на инт; } class RectangleClass {public int width; обществена височина на инт; }

Следният код профилира ефективността на използването на масив от структури срещу масив от класове. За целите на илюстрацията използвах милион обекта и за двете, но обикновено не се нуждаете от толкова много обекти във вашето приложение.

static void Main (string [] args) {const int size = 1000000; var structs = нов RectangleStruct [размер]; класове var = нов RectangleClass [размер]; var sw = нов хронометър (); sw.Start (); for (var i = 0; i <size; ++ i) {structs [i] = new RectangleStruct (); structs [i] .breadth = 0 structs [i] .height = 0; } var structTime = sw.ElapsedMilliseconds; sw.Reset (); sw.Start (); за (var i = 0; i <размер; ++ i) {класове [i] = нов RectangleClass (); класове [i] .breadth = 0; класове [i] .height = 0; } var classTime = sw.ElapsedMilliseconds; sw.Stop (); Console.WriteLine ("Време, взето от масив от класове:" + classTime.ToString () + "милисекунди."); Console.WriteLine ("Време, отнето от масив от структури:" + structTime.ToString () + "милисекунди."); Console.Read (); }

Програмата е проста: Създава 1 милион обекта от структури и ги съхранява в масив. Той също така създава 1 милион обекта от клас и ги съхранява в друг масив. Ширината и височината на свойствата се присвояват на стойност нула за всеки екземпляр.

Както можете да видите, използването на удобни за кеша структури осигурява огромна печалба в производителността.

Правила за по-добро използване на кеша на процесора

И така, как се пише код, който най-добре използва кеша на процесора? За съжаление няма магическа формула. Но има някои основни правила:

  • Избягвайте да използвате алгоритми и структури от данни, които показват нередовни модели на достъп до паметта; вместо това използвайте линейни структури от данни.
  • Използвайте по-малки типове данни и организирайте данните, така че да няма дупки за подравняване.
  • Помислете за моделите на достъп и се възползвайте от линейните структури от данни.
  • Подобряване на пространственото местоположение, което използва всяка линия на кеша в максимална степен, след като бъде картографирана в кеш.