Защо повечето езици от високо ниво са бавни
През последните няколко месеца, често ми задават този въпрос, така че реших да му отговорите статия.
Причините, поради които повечето езици за програмиране от високо ниво са бавни, обикновено две:
- те не работят добре с кеш;
- събиране на боклука отнема много ресурси.
В действителност, тези два фактора са сведени до един - в такива езици са голям брой разпределения на паметта.
Необходимо е да се изясни, че аз говоря предимно за клиентски приложения, където е важно местната скорост на изпълнение. Ако заявлението се харчат 99.9% от времето в очакване на отговор от мрежата е малко вероятно да има значение колко бавно е език проблема за оптимизиране на мрежата е по-важно.
Аз ще взема за пример C #, по две причини: първо, език на високо равнище, която използвам често напоследък, и второ, ако бях взел Java, феновете на C # може да се твърди, че използването на стойност типове в C # се елиминират по-голямата част от проблеми (не е).
В допълнение, ще процедира по презумпцията, че писменото идиоматичен кода в същия стил със стандартните библиотеки, и като се вземат предвид споразуменията, приети. Аз няма да покрие грозни патерици като "решение" на проблема. Да, в някои случаи тя позволява да се заобиколят особености на езика, но не елиминира първоначалния проблем.
Работа с кеша
За начало, нека да разгледаме защо е важно да се използва кеша правилно. Ето една схема на латентността на паметта за Haswell процесори, базирани на тези данни.
Забавянето на четене от купчина около 230 цикъла, докато, както четенето на кеша L1 - 4 цикъла. Оказва се, че не работи правилно с кеша може да направи кода почти 50 пъти по-бавно. Освен това, благодарение на модерна многозадачна процесор, четене от кеша могат да възникнат по време на работа с регистрите, така че забавяне на четене от L1 може да бъде пренебрегнато.
Казано по-просто, в допълнение към развитието на алгоритъма, кеш пропуска - основният проблем на производителността. След като решите проблема с ефективен достъп до данни, ще има само малки оптимизации, че в сравнение с несъвпадение на кеша, не толкова влияние върху производителността.
Добра новина за разработчиците на език: не е нужно да се развива по най-ефективния компилатор и можете да спестите време, като използвате някои абстракция. Всичко, което трябва - да се организира ефективен достъп до данни и програми в областта на езика, ще бъде в състояние да се конкурират в скоростта на изпълнение с В.
Защо да използвате C # е причина за чести кеш пропуска
Казано по-просто, C # е проектиран по такъв начин, че да не се вземат предвид съвременните реалности на работа с кеш. За пореден път, аз не говоря за ограниченията, наложени от език и "натиск", че те трябва да си програмист, което прави писането не е най-ефективният код. Теоретично, има временно решение, макар и не най-удобния за много от тези неща. Говоря за кода, който ни насърчава да пишат на езика като такъв.
Основният проблем на C # е, че тя е много слаба подкрепа стойност типове. Да, има има структури, които представляват ценност, съхранявана в едно и също място и обяви, (например, стека или вътре в други обекти). Но има и някои доста сериозни проблеми, поради които тези структури са по-патерици, отколкото решения.
- От вас се изисква да декларира типа на данните, по-рано, което означава, че ако някога ви се наложи да получите на обекта от този тип са съществували в купчина, всички обекти от този тип винаги ще бъдат разпределени на куп. Разбира се, можете да напишете обвивка клас за желаните типове данни, както и използване във всеки случай желания обвивка, но това е доста болезнен начин. Би било добре, ако класовете и структури винаги са обявени за едни и същи, но тяхното използване е възможно по различни начини и това е както трябва в този случай. . Това е, когато имате нужда от нещо в стека, можете да го декларира като стойност, а когато купчината - като обект. Тя работи така C ++, например. Там не са принудени да правят всички обекти, дори и само защото има някои неща, които трябва да са на куп (по различни причини).
- Възможността за връзки са много ограничени. Можете да мине всичко върху връзката към функцията, но това е всичко. Не може просто да отидете и да получите линк към елемент от списъка
. например. За да направите това, трябва да се запази в списъка и на връзките и стойности едновременно. Невъзможно е да получим указател към нещо, разпределени в стека или са разположени във вътрешността на обекта. Можете да копирате само всички или вижте функцията за връзка. Причините за подобно поведение, разбира се, са съвсем ясни. Ако безопасност тип приоритет, че е доста трудно (ако не и невъзможно) да се поддържа гъвкава система от връзки и в същото време да се гарантира тази сигурност. Но дори и разумността на тези ограничения не променя факта, че тези ограничения са все още там. - Буфери с фиксиран размер не поддържа дефинирани от потребителя типове и изискват опасен директива.
- Ограничена функционалност субредове. Има ArraySegment клас. но малко по-използвани. Ако трябва да се премине набор от елементи на масива, трябва да създадете IEnumerable. и следователно достатъчно памет. Дори ако API поддържа ArraySegment. това все още не е достатъчно, не можете да го използвате за всеки Списък
. нито за масива в стека. само за обичайния набор.
В резултат на това на езика на всички, но най-простите случаи, избутва в активната употреба на куп. Ако всичките ви данни в купчината, вероятността за мис е по-висока (защото не може да се реши как да се организира данните). Докато пишете програма на С ++ програмист трябва да мисля за това как ефективно да се осигури достъп до данните, C # насърчава тяхното местоположение в някои части на паметта. Програмист загуби контрол върху разпределението на данни, което води до ненужни грешки и, като следствие, спада на производителността. А фактът, че можете да се състави C # програма в родния код. това няма значение. Ръстът на производителността не компенсира лошото местност кеш.
Разбира се, има заобикаляне. Например, можете да използвате устройството и да ги върне в пула от Списъка
И накрая, искам да подчертая, че този проблем възниква не само в тесните места. В idiomatically писмен код референтните типове са навсякъде. Това означава, че през цялото време на закъснението код винаги ще се появи в няколко стотин цикли, които обхващат всички оптимизации. Ако дори оптимизира области ресурсоемки кодът ви ще бъде също толкова бавно. Така че, ако не искате да се пишат програми, използващи басейните с памет и индексите, които работят, може би по-ниско ниво, отколкото C ++ (тогава защо употреба C #), едва ли нещо, което може да направи, за да се избегне този проблем.
събиране на боклука
Предполагам, че читателят разбира защо събиране на боклука по себе си е сериозен проблем за изпълнение. Случайни увисва при натрупване на времето, това е неприемливо за анимационни програми. Няма да се спирам на него и преминете към как да проектирате самия език може да влоши положението.
Поради ограничения език за работа с ценностни видове, разработчиците често имат вместо една голяма структури от данни (които дори могат да бъдат пуснати в стека) използват много малки обекти, които са разположени в купчина. В същото време не трябва да забравяме, че колкото повече обекти са поставени в една купчина, толкова повече работа за боклукчията.
Има някои тестове, които C # и Java са превъзхожда C ++ в представянето си, тъй като разпределението на паметта на език, с разходите по събирането на боклука са много по-евтино. На практика обаче, това се случва рядко. Да се напише програма на C # в същия брой разпределения на паметта, както и в най-наивно изпълнението на C ++, изисква много повече усилия. Сравняваме силно оптимизиран код на езика с автоматично управление на паметта с родния програма, написана "на челото." Ако прекарате един и същ период от време, оптимизиране на C ++ код, ние отново напусне C # зад себе си.
Знам, че е възможно да се напише боклучар, която осигурява висока производителност (например, постепенно, с определено време за събиране), но това не е достатъчно. Най-големият проблем е езика на високо ниво - че те насърчават създаването на голям брой обекти. Ако в идиоматичен C # ще бъде един и същ номер на разпределението на паметта, както и в програмата в C, събиране на боклука представена щеше да е много по-малък проблем. И ако сте били при използване на пределната събирач на боклука, например, системи за мека реално време, най-вероятно ще се нуждаят от бариера, за да го влизане. Но без значение колко евтини, че не се лекува, на език, който да насърчава активното използване на указатели, което означава допълнително забавяне, когато маржовете са се променили.
Погледнете .Net стандартната библиотека - разпределението на паметта на всяка крачка. Според моите изчисления .Net Framework ядро съдържа 19 пъти повече, отколкото обществени класи структури. Това означава, че докато го използвате вие ще се сблъскате с голям брой разпределение. Дори създателите на езика не може да устои на изкушението! Аз не знам как да се събират статистически данни, но с помощта на базовия клас библиотека, ще забележите, със сигурност, че огромно количество памет разпределяне се извършва през целия кода. Всички код е написан въз основа на предположението, че това е евтин експлоатация. Ти дори не може да донесе вътр към екрана без да се създава обект! Представете си: дори с помощта на StringBuilder не можете просто да го мине без отпускане вътр посредством стандартната библиотека. Това е глупаво.
Той се намира не само в стандартната библиотека. Дори и в API единство (гейм енджин. За което изпълнение е важно) навсякъде методи, които връщат препратки към обекти или вида референтни масиви или принуждаване на потребителя да разпредели памет, за да се позове на тези методи. Например, връщайки масив от GetComponents. разработчиците заделят памет за масив, само за да види какво компоненти са в GameObject. Разбира се, има и алтернатива API, но прави ако следвате стила на език, ненужни разпределения памет, не могат да бъдат избегнати. Момчетата от Unity да пишат "добър» C #, която има ужасно представяне.
заключение
Ако се развие нов език, pozhaluycta. Помислете за изпълнение. Не "достатъчно умен съставител" не може да му предостави постфактум, ако не положи тази опция първоначално. Да, това е трудно да се осигури безопасността на вид, без събиране на боклука. Да, това е трудно да се съберат боклука, ако данните не е еднаква. Да, това е трудно да се разрешат въпросите на обхват, ако можете да получите указател към всяко място в паметта. Да, има много проблеми. Но развитието на нов език - решаването на тези проблеми. Защо направи нов език, ако не е много по-различен от тези, които са били създадени през 60-те?
Дори и да не може да реши всички проблеми, можете да се опитате да се справят с повечето от тях. Можете да използвате видовете регион в Руст за безопасност. Възможно е да се откаже от идеята за "безопасност вид на всяка цена", увеличаване на броя на време на работа, проверки (ако те не причиняват допълнителни кеш пропуска). Covariant масиви в C # - е точно така. В действителност, този вид байпас система, което води до освобождаването на изключение по време на изпълнение.
Обобщавайки, можем да кажем, че ако искате да се разработи C ++ алтернатива на vysokoprozvoditelnogo код, трябва да се мисли за местоположението на данните и местността.