Кіріспе
Бағдарламалық жасақтаманың тиімділігін арттыру
Компьютерлік ғылымда бағдарламаны оңтайландыру, кодты оңтайландыру немесе бағдарламалық жасақтаманы оңтайландыру – бұл бағдарламалық жүйенің қандай да бір аспектісін тиімдірек жұмыс істеуі немесе аз ресурстарды қолдануы үшін өзгерту процесі. Әдетте, компьютерлік бағдарлама жылдам орындалуы, аз жадты пайдалануы немесе басқа ресурстарды үнемдеуі, немесе аз қуатқа жұмсалуы үшін оңтайландырылуы мүмкін.
Жалпы
«Оптимизация» сөзі «оптималды» сөзінің түбірімен ортақ болғанымен, оптимизация процесінің шын мәнінде оптималды жүйеге қол жеткізуі сирек. Жүйені, әдетте, абсолютті түрде емес, тек берілген сапа көрсеткіші бойынша ғана оңтайландыруға болады, бұл көрсеткіш басқа мүмкін көрсеткіштерге қайшы келуі мүмкін. Нәтижесінде, оңтайландырылған жүйе әдетте бір қолданбада немесе бір аудитория үшін ғана оптималды болады. Бағдарламаның белгілі бір міндетті орындау уақытын қысқарту үшін оны көбірек жадты пайдалануға мәжбүрлеуге болады. Егер жад кеңістігі шектеулі болса, жадты аз қолдану үшін баяу алгоритмді әдейі таңдауға болады. Көбінесе барлық жағдайларда жақсы жұмыс істейтін «бір өлшемге сай» дизайн болмайды, сондықтан инженерлер ең маңызды қасиеттерді оңтайландыру үшін шартты келісімдерге барады. Сонымен қатар, бағдарламалық құралды толығымен оңтайландыруға, оны одан әрі жақсарту мүмкін болмайтындай етуге жұмсалатын күш-жігер, көбінесе күтілетін пайдадан асып түседі; сондықтан оңтайландыру процесі толыққанды оптималды шешімге қол жеткізбей тоқтатылуы мүмкін. Құрметтісіміз, көбінесе үлкен жетістіктер процестің басында болады. Тіпті белгілі бір сапа көрсеткіші үшін (мысалы, орындалу жылдамдығы) оптимизацияның көптеген әдістері нәтижені жақсартуға ғана бағытталған, олар оптималды нәтиже беруді мақсат етпейді. Супероптимизация – бұл шынымен оптималды нәтижені табу процесі.
Оптимизациялау деңгейлері
Оптимизация бірнеше деңгейде жүзеге асуы мүмкін. Әдетте, жоғары деңгейлер үлкен әсер етеді және жобаның кейінгі сатыларында өзгерту қиын, егер өзгерту қажет болса, маңызды түзетулер немесе толық қайта жазу талап етіледі. Осылайша, оптимизация көбінесе жоғарыдан төменге қарай жетілдіру арқылы жүргізіледі, ал бастапқы нәтижелер үлкен болып, аз жұмыспен қол жеткізіледі, ал кейінгі нәтижелер кішкентай болып, көбірек жұмыс талап етеді. Дегенмен, кейбір жағдайларда жалпы өнімділік бағдарламаның өте төмен деңгейдегі бөліктерінің өнімділігіне байланысты болады, ал соңғы кезеңде жасалған шағын өзгертулер немесе төмен деңгейдегі егжей-тегжейлі қарастырудың ерте кезеңінде жасалған өзгертулер күрт әсер етуі мүмкін. Жоба бойында тиімділікке назар аударылады, бірақ бұл айтарлықтай өзгереді, бірақ маңызды оптимизация көбінесе кейінге қалдырылатын жетілдіру ретінде қарастырылады. Ұзақ мерзімді жобаларда оптимизация циклдары жиі кездеседі, бір саланың жақсартуы екіншісіндегі шектеулерді көрсетеді, және олар әдетте өнімділік қанағаттанарлық болғанда немесе пайда тым аз немесе қымбат болғанда тоқтатылады. Бағдарламаның сипаттамасының бір бөлігі ретінде өнімділік маңызды, өйткені пайдалануға тым баяу бағдарлама мақсатына сай емес: 60 Гц (секундына кадрлар) жиілігіндегі видео ойын қанағаттанарлық, бірақ секундына 6 кадр - қабылдауға болмайтын, ұсақ-түйек өнімділік. Жүйенің жеткілікті өнімділікті қамтамасыз ете алатынына көз жеткізу үшін бастапқы кезеңнен бастап өнімділік қарастырылады, ал бастапқы прототиптер соңғы жүйенің (оптимизациямен) қанағаттанарлық өнімділікке жететініне сенімді болу үшін шамамен қанағаттанарлық өнімділікке ие болуы керек. Кейде оптимизацияны кейін жасауға болады деген сеніммен бұл қадам жіберіліп қойылады, нәтижесінде прототиптік жүйелер тым баяу болады, көбінесе бір немесе одан да көп есе, және жүйелер архитектуралық тұрғыдан өнімділік мақсаттарына жете алмайды, мысалы Intel 432 (1981); немесе Java (1995) сияқты, HotSpot (1999) пайда болғанға дейін қанағаттанарлық өнімділікке жете алмады. Прототип пен өндірістік жүйе арасындағы өнімділіктің өзгеру дәрежесі және оның оптимизацияға бейімділігі маңызды белгісіздік пен тәуекел көзі болуы мүмкін.
Жобалау деңгейі
Жоғарғы деңгейде жоба қолданылатын ресурстарды, мақсаттарды, шектеулерді және күтілетін қолдануды/жүктемені ең жақсы пайдалану үшін оңтайландырылуы мүмкін. Жүйенің архитектуралық дизайны оның өнімділігіне айқын әсер етеді. Мысалы, желілік кешігумен шектелген жүйе (желілік кешігу жалпы өнімділіктің басты шектеуі болатын жағдайда) желілік сапарларды азайту үшін оңтайландырылады, идеалды жағдайда көптеген қадамдардың орнына бір ғана сұрау (немесе түрту протоколындағыдай, сұрауларсыз) жасалады. Дизайнды таңдау мақсаттарға байланысты: компиляторды жобалау кезінде, егер жылдам компиляция басты мақсат болса, бір өтетін компилятор бірнеше өтетін компилятордан жылдам болады (бірдей жұмыс көлемі болғанда), бірақ егер шығыс кодтың жылдамдығы мақсат болса, баяурақ бірнеше өтетін компилятор мақсатқа жақсырақ жетеді, тіпті ол көбірек уақыт алса да. Платформа мен бағдарламалау тілін таңдау осы деңгейде жүзеге асырылады және оларды жиі өзгерту толық қайта жазуды қажет етеді, бірақ модулдік жүйе кейбір компоненттерді ғана қайта жазуға мүмкіндік береді, мысалы, Python бағдарламасы өнімділікке маңызды бөлімдерді C тілінде қайта жаза алады. Таратылған жүйеде архитектураны таңдау (клиент-сервер, теңдестірілген жүйе және т.б.) жобалау деңгейінде жүзеге асырылады және оны өзгерту қиын болуы мүмкін, әсіресе егер барлық компоненттерді бірдей уақытта алмастыру мүмкін болмаса (мысалы, ескі клиенттер).
Алгоритмдер мен деректердің құрылымы
Жалпы жобаны ескере отырып, тиімді алгоритмдер мен деректер құрылымдарын таңдау және осы алгоритмдер мен деректер құрылымдарын тиімді іске асыру келесі қадам болып табылады. Жобалаудан кейін, алгоритмдер мен деректер құрылымдарын таңдау бағдарламаның кез келген басқа аспектісіне қарағанда тиімділікке көбірек әсер етеді. Әдетте деректер құрылымдарын алгоритмдерге қарағанда өзгерту қиынырақ, себебі деректер құрылымының болжамдары мен оның жұмыс істеу қағидалары бағдарламаның көп бөлігінде қолданылады. Алайда, функциялар анықтамаларында абстрактілі деректер түрлерін пайдалану және нақты деректер құрылымының анықтамаларын бірнеше орынға ғана шектеу арқылы бұл мәселені азайтуға болады. Алгоритмдер үшін бұл, негізінен, алгоритмдердің кіріс деректеріне (уақыт және жад) қатысты константалық O(1), логарифмдік O(log n), сызықтық O(n) немесе кейбір жағдайларда логарифмдік-сызықтық O(n log n) күрделілікке ие болуын қамтамасыз етуден тұрады. Квадраттық күрделілігі O(n2) алгоритмдер масштабтауға қабілетсіз, тіпті сызықтық алгоритмдер де қайта-қайта шақырылғанда проблема тудыруы мүмкін, сондықтан мүмкіндігі болса, олар тұрақты немесе логарифмдік алгоритмдермен ауыстырылады. Асимптотикалық өсу ретінен басқа, тұрақты коэффициенттер де маңызды: асимптотикалық жайлау алгоритм кішкентай кіріс деректерімен жұмыс істегенде жылдам немесе кішірек болуы мүмкін (себебі оның құрылымы қарапайым), бұл нақты жағдайда орын алуы мүмкін. Көбінесе гибридтік алгоритм ең жақсы өнімділікті қамтамасыз етеді, себебі бұл қатынас деректер көлеміне байланысты өзгереді. Өнімділікті жақсартудың жалпы әдісі – жұмысты азайту. Мысалы, жиі кездесетін жағдайлар үшін жылдам жолды пайдалану, артық жұмысты болдырмау арқылы өнімділікті арттыруға болады. Мысалы, латын әліпбиіндегі мәтін үшін қарапайым мәтіндік орналасу алгоритмін қолданып, күрделі жазу жүйелері үшін ғана (мысалы, Деванагари) күрделі орналасу алгоритміне ауысу. Тағы бір маңызды техника – кэштеу, әсіресе мемоизация, ол қайталама есептеулерден сақтайды. Кэштеудің маңыздылығынан, жүйеде көбінесе кэштеудің көп деңгейлері болады, бұл жадты пайдалану және ескі кэштерден туындайтын дұрыс емес нәтижелер сияқты проблемаларды тудыруы мүмкін.
Көлік кодының деңгейі
Жалпы алгоритмдер мен оларды абстрактілі машинада іске асырудан өзгеше, нақты бастапқы код деңгейіндегі таңдаулар маңызды айырмашылықтар тудыруы мүмкін. Мысалы, C тілінің алғашқы компиляторларында, шартты емес цикл үшін while(1) конструкциясы for(;;) конструкциясынан баяу болды, себебі while(1) 1-ді бағалап, содан кейін оның мәні шындыққа сәйкес келе ме екенін тексертін шартты секіруді орындады, ал for(;;) конструкциясы тікелей шартсыз секіруді жасады. Кейбір оңтайландырулар (осыған ұқсас) қазіргі таңдағы оңтайландырылатын компиляторлар арқылы жүзеге асырылуы мүмкін. Бұл бастапқы тілге, мақсатты машина тіліне және компиляторға байланысты, оны түсіну немесе болжау қиын болуы мүмкін, сондай-ақ уақыт өте келе өзгеруі де мүмкін. Осы себепті компиляторлар мен машиналық кодты түсіну өнімділікті арттыруға көмектеседі. Цикл өзгермейтін кодты жылжыту және қайтарылатын мәнді оңтайландыру – бұл қосымша айнымалылардың қажеттілігін азайтатын және тіпті айналымды оңтайландырулардан аулақ болып, өнімділікті арттыруға мүмкіндік беретін оңтайландырулардың мысалдары.
Құрылыс деңгейі
Көздік код пен компиляция деңгейі арасында, директивалар мен құрастыру опциялары көздік кодтағы және компилятордағы өнімділік параметрлерін жақсарту үшін қолданылуы мүмкін, мысалы, қажетсіз бағдарламалық мүмкіндіктерді өшіру үшін препроцессор анықтамаларын пайдалану, нақты процессор модельдеріне немесе аппараттық қабілеттерге оңтайландыру, немесе тармақталуды болжау сияқты. BSD Ports және Gentoo Portage сияқты көздік кодқа негізделген бағдарламалық қамтамасыз ету тарату жүйелері осы түрдегі оңтайландырудан пайдалана алады.
Компиляция деңгейі
Оптимизациялайтын компиляторды қолдану нәтижесінде, атқарылатын бағдарлама компилятордың болжай алатын деңгейінде кем дегенде сондай оптимизацияланатынына көз жеткізіледі.
Құрастыру деңгейі
Ең төменгі деңгейде, нақты бір аппараттық платформаға арналған тілде код жазу, егер бағдарламашы машиналық нұсқаулардың барлық мүмкіндіктерін пайдаланса, ең тиімді және ықшам кодты жасауға мүмкіндік береді. Осы себепті, кіріктірілген жүйелерде қолданылатын көптеген операциялық жүйелер дәстүрлі түрде ассемблер тілінде жазылған. Бағдарламалар (өте кішкентай бағдарламалардан басқа) сирек жағдайларда бастапқы кодтан соңына дейін ассемблерде жазылады, себебі бұл көп уақыт пен шығынды талап етеді. Көбінесе жоғары деңгейдегі тілден ассемблерге түрлендіріліп, содан кейін қолмен оңтайландырылады. Тиімділік пен көлем маңызды болмаған жағдайда, кодтың үлкен бөлігі жоғары деңгейдегі тілде жазылуы мүмкін. Заманауи оңтайландырушы компиляторлардың және жаңа процессорлардың күрделілігінің арқасында, компилятор жасаған коддан тиімді код жазу қиын, және көптеген жобаларға осы "соңғы" оңтайландыру қадамы қажет емес. Бүгінде жазылатын кодтың көп бөлігі мүмкіндігінше көп машинада жұмыс істеуге бағытталған. Осының салдарынан, бағдарламашылар мен компиляторлар әрқашан жаңа процессорлардың ұсынатын тиімді нұсқауларын немесе ескі үлгілердің ерекшеліктерін толыққанды пайдаланбайды. Сонымен қатар, мұндай нұсқауларды қолданбай, нақты бір процессорға баптаулы ассемблер коды басқа процессорда да оңтайлы болмауы мүмкін, кодтың басқа бапталуын күтуде. Көбінесе, бүгінде ассемблер тілінде жазудың орнына, бағдарламашылар компилятордың нәтижесін талдау және жоғары деңгейдегі бастапқы кодты тиімдірек түрлендірілуі үшін өзгерту, немесе оның тиімсіздігінің себебін түсіну үшін дисасемблерді пайдаланады.
Орындалу уақыты
Жақын уақытта компиляторлар орындалу уақытындағы деректерге негізделген, компиляцияға кеткен уақыт есебінен, жеке машиналық кодты жасай алады. Бұл техника ең алғашқы рет түрақты өрнектерді өңдеуге арналған қозғалтқыштарда қолданылды және Java HotSpot және JavaScript үшін V8 арқылы кеңінен таралды. Кейбір жағдайларда, адаптивті оптимизациялау нақты кіріс немесе басқа факторларға сәйкес параметрлерді динамикалық түрде реттеу арқылы статикалық компиляторлардың мүмкіндіктерін асыратын орындалу уақытын оптимизациялай алады. Профильге бағытталған оптимизация – бұл орындалу уақытының профильдеріне негізделген, уақытынан бұрын (AOT) компиляцияны оңтайландыру әдісі және адаптивті оптимизацияның динамикалық техникасының статикалық "орташа жағдай" аналогына ұқсас. Өзін-өзі өзгертетін код, кодты оңтайландыру үшін орындалу уақытының жағдайларына жауап ретінде өзін өзгерте алады; мұндай жағдайлар жиі ассемблер тілінде жазылған бағдарламаларда кездеседі. Кейбір процессорлардың архитектурасы орындалу кезінде белгілі бір оптимизацияларды жүзеге асыра алады. Мысалдарға реттіліксіз орындалу, болжамды орындалу, нұсқау конвейерлері және тармақ болжаушылар жатады. Компиляторлар бағдарламаға осы процессор мүмкіндіктерін пайдалануға көмектеседі, мысалы, нұсқауларды жоспарлау арқылы.
Платформаға тәуелді және тәуелсіз оңтайландырулар
Кодты оңтайландыруды платформаға тәуелді және платформаға тәуелсіз техникалар ретінде де кеңінен жіктеуге болады. Соңғылары көптеген немесе барлық платформаларда тиімді болса, платформаға тәуелді техникалар бір платформаның нақты қасиеттерін пайдаланады немесе жалғыз платформаға немесе тіпті жалғыз процессорға байланысты параметрлерге сүйенеді. Сондықтан, әртүрлі процессорлар үшін бірдей кодтың әртүрлі нұсқаларын жазу немесе жасау қажет болуы мүмкін. Мысалы, компиляция деңгейіндегі оңтайландыруда платформаға тәуелсіз техникалар – циклды ашу, функция шақыруларын азайту, жадты тиімді пайдалану, шарттарды азайту сияқты жалпы техникалар болып табылады, олар көптеген CPU архитектураларына ұқсас әсер етеді. Платформаға тәуелсіз оңтайландырудың жарқын мысалы – ішкі for циклы, онда ішкі for циклы бар циклдың бірлік уақытта ішкі циклы жоқ немесе while циклы бар циклға қарағанда көбірек есептеулер жасайтыны байқалды. Жалпы алғанда, бұл бағдарламаны аяқтау үшін қажетті нұсқаулар тізбегінің ұзындығын қысқартуға және/немесе процесс кезіндегі жадты пайдалануды азайтуға көмектеседі. Ал платформаға тәуелді техникаларға нұсқауларды жоспарлау, нұсқау деңгейіндегі параллелизм, дерек деңгейіндегі параллелизм, кэшті оңтайландыру техникалары (яғни, әртүрлі платформаларда өзгеше болатын параметрлер) жатады, сонымен қатар оңтайлы нұсқауды жоспарлау тіпті бір архитектураның әртүрлі процессорларында да әртүрлі болуы мүмкін.
Компромистік факторлар
Дегенмен, кейбір жағдайларда оңтайландыру күрделірек алгоритмдерді қолдануға, "арнайы жағдайларды" және "ерекше тәсілдерді" пайдалануға, сондай-ақ күрделі шартты келісімдерге негізделеді. "Толық оңтайландырылған" бағдарламаны түсіну қиын болуы мүмкін, сондықтан оңтайландырылмаған нұсқалармен салыстырғанда оның қателерге ұшырау ықтималдығы жоғары. Көзге көрінетін антипаттерндерді жоюдан өзге, кейбір код деңгейіндегі оңтайландырулар қолдануға ыңғайлылығын төмендетуі мүмкін. Оңтайландыру көбінесе өнімділіктің бір немесе екі аспектісін жақсартуға бағытталған: орындалу уақыты, жадты пайдалану, дискідегі орын, өткізу қабілеті, қуатты тұтыну немесе басқа да ресурстар. Мұндай жағдайда, әдетте, бір факторды жақсарту үшін басқаларын құрбан ету қажет болады. Мысалы, кэштің көлемін арттыру орындалу уақытын жақсартады, бірақ жадты тұтынуды да арттырады. Кодтың түсініктілігі мен лакониктілігі де жиі кездесетін шартты келісімдердің нысаны болып табылады. Оңтайландыруды жүзеге асыратын бағдарламашы кейбір операциялар үшін бағдарламалық құралды жақсартуға шешім қабылдауы мүмкін, бірақ басқа операциялардың тиімділігін төмендету есебінен. Мұндай шартты келісімдер кейде техникалық емес сипатқа ие болуы мүмкін, мысалы, бәсекелес компанияның коммерциялық сәттілікті арттыру үшін жеңілуі керек болған көрсеткішіне қол жеткізу, бірақ бұл бағдарламалық құралды қалыпты пайдалануды тиімсіз етумен байланысты болуы мүмкін. Мұндай өзгерістер кейде әжуалы түрде "пессимизация" деп аталады.
Қиындықтар
Оптимизация жүйедегі өнімділікті шектейтін фактор – бөтелке табуды қамтиды. Код жағынан алғанда, бұл көбінесе қажетті ресурсты ең көп пайдаланатын кодтың маңызды бөлігі болады, бірақ бұл I/O кідірісі немесе желілік өткізу қабілеті сияқты басқа факторлар да болуы мүмкін. Компьютер ғылымында ресурстарды тұтыну көбінесе қуат заңына сәйкес келеді, ал Парето принципі ресурстарды оңтайландыру үшін қолданылуы мүмкін, себебі ресурстардың 80% көбінесе операциялардың 20% бөлігімен пайдаланылады. Бағдарламалық жасақтауда компьютерлік бағдарламаның орындалу уақытының 90% кодтың 10% бөлігін орындауға жұмсалады деген шамалау жиі кездеседі (бұл жағдайда 90/10 заңы деп аталады). Күрделі алгоритмдер мен деректер құрылымдары көп мөлшердегі деректермен жақсы жұмыс істейді, ал қарапайым алгоритмдер шағын деректерге арналған. Күрделі алгоритмнің орнату, инициализация уақыты және тұрақты факторлары артықшылықты басып тастауы мүмкін, сондықтан гибридтік немесе бейімделмелі алгоритм жеке алгоритмге қарағанда жылдам болуы мүмкін. Қандай функционалдық мүмкіндіктер қандай шарттарға сәйкес келеді деген шешімдерді қабылдау үшін өнімділік профильдеушісін пайдалануға болады. Кейбір жағдайларда жадты көбейту бағдарламаның жылдам жұмыс істеуіне көмектеседі. Мысалы, сүзгілеу бағдарламасы әдетте әрбір жолды оқып, сүзіп, дереу шығарады. Бұл тек бір жолға жететіндей жадты пайдаланады, бірақ әрбір дискіні оқудың кідірісіне байланысты өнімділік көбінесе нашар болады. Нәтижені кэштеу де тиімді, бірақ оған көбірек жад қажет.
Оптимизациялау уақыты
Кейде, оны жүзеге асыруға кеткен оптимизациялау уақыты өзі мәселе тудыруы мүмкін. Қолданыстағы кодты оңтайландыру көбінесе жаңа мүмкіндіктерді қоспайды, тіпті нашаррақ, бұрын жұмыс істейтін кодқа жаңа қателер енгізе алады (кез келген өзгеріс сияқты). Қолмен оңтайландырылған кодтың кейде оңтайланбаған кодқа қарағанда "оқырлығы" нашар болуы мүмкін, сондықтан оңтайландыру оның қолдау-күтімге де әсер етуі мүмкін. Оптимизацияның бағасы бар, және бұл инвестицияның тиімділігіне көз жеткізу маңызды. Автоматты оптимизатор (немесе оңтайландырушы компилятор, кодты оңтайландыруды жүзеге асыратын бағдарлама) өзі де оңтайландырылуы мүмкін, мақсатты бағдарламаларының тиімділігін одан әрі жақсарту үшін немесе өзінің жұмысын жылдамдату үшін. Оптимизация қосылған компиляция көбінесе ұзаққа созылады, бірақ бұл әдетте тек бағдарламалар өте үлкен болғанда ғана проблема болады. Әсіресе, дереу компиляциялайтын компиляторлар үшін, орындалу кезіндегі компиляциялау компонентінің өнімділігі, оның мақсатты кодымен бірге жұмыс істеуі, жалпы орындалу жылдамдығын арттырудың кілті болып табылады.