C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Ի՞նչն է ազդում C++ ծրագրերի արագության վրա և ինչպես հասնել դրան կոդի բարձր մակարդակում: CatBoost գրադարանի առաջատար մշակող Եվգենի Պետրովը պատասխանել է այս հարցերին՝ օգտագործելով օրինակներ և նկարազարդումներ CatBoost-ի վրա x86_64-ի համար աշխատելու իր փորձից:

Տեսահաշվետվություն

Խաղալ տեսանյութ


- Բարեւ բոլորին։ Ես օպտիմիզացնում եմ CatBoost մեքենայական ուսուցման գրադարանը պրոցեսորի համար: Մեր գրադարանի հիմնական մասը գրված է C++-ով։ Այսօր ես ձեզ կասեմ, թե ինչ պարզ եղանակներով ենք մենք արագության հասնում:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Հաշվարկների արագությունը բաղկացած է երկու մասից. Առաջին մասը ալգորիթմն է։ Եթե ​​մենք սխալվենք ալգորիթմի ընտրության հարցում, ապա մենք չենք կարողանա այն արագ աշխատել: Երկրորդ մասն այն է, թե որքանով է օպտիմիզացված մեր ալգորիթմը մեր ունեցած հաշվողական համակարգի համար՝ իր կատարողականությամբ և թողունակությամբ:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Տվյալների փոխանակումը և հաշվարկները պետք է հաշվի առնվեն առանձին՝ դրանց արագության մեծ տարբերության պատճառով: Եթե ​​հիշողության արագությունը համարենք հետիոտնի արագությունը, ապա հաշվարկի արագությունը մոտավորապես մարդատար ինքնաթիռի նավարկության արագությունն է։

Այս տարբերությունը հարթելու համար ճարտարապետությունն ունի քեշավորման մի քանի մակարդակ: Ամենաարագը և ամենափոքրը L1 քեշն է: Այնուհետև կա ավելի մեծ և դանդաղ երկրորդ մակարդակի քեշ: Եվ կա շատ մեծ քեշ, որը կարող է լինել տասնյակ մեգաբայթեր, երրորդ մակարդակի քեշ, բայց դա ամենադանդաղն է։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Հաշվարկների տվյալների փոխանակման տարբեր արագության պատճառով հաշվողական կոդը բաժանվում է երկու դասի. Մեկ դասը սահմանափակված է թողունակությամբ, այսինքն՝ տվյալների փոխանակման արագությամբ։ Երկրորդ դասը սահմանափակվում է պրոցեսորի արագությամբ: Նրանց միջև սահմանը սահմանվում է կախված այն գործողությունների քանակից, որոնք կատարվում են տվյալների մեկ բայթով: Սա սովորաբար կոդի հատուկ հաստատուն է:

Ծանր հաշվողական կոդի մեծ մասը գրված է երկար ժամանակ, շատ լավ օպտիմիզացված է, և կան մեծ թվով գրադարաններ, ուստի իմաստ ունի, եթե ձեր կոդի մեջ ծանր հաշվարկ եք տեսնում, փնտրել գրադարան, որը կարող է անել: դա ձեզ համար:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Մնացածից կոմպիլյատորները չեն կարող ամեն ինչ անել, քանի որ դրանց զարգացման վրա ծախսվում է ռեսուրսների շատ սահմանափակ տոկոս: Նրանցից որո՞նք են այսօր քիչ թե շատ ակտիվ զարգանում, այսինքն՝ սատարում են ստանդարտներին ու փորձում վերահսկել դրանք։ Սա ճակատային EDG է, որն օգտագործվում է տարբեր ածանցյալ գործիքներում, ինչպիսիք են Intel կոմպիլյատորը; LLVM; GNU և Microsoft Frontend:

Քանի որ դրանք քիչ են, կոմպիլյատորներն աջակցում են միայն հաճախականության վերահսկման օրինաչափություններին և տվյալների կախվածությանը: Եթե ​​նայենք հսկողությանը, ապա դրանք գծային հատվածներ և պարզ ցիկլեր են, այսինքն՝ հրահանգների և կրկնությունների հաջորդականություն: Նրանք հաճախականության կախվածությունը սովորում են կրճատման տվյալներից, երբ մենք, ասենք, շատ տարրեր գումարում ենք մեկի մեջ, փլվում և կատարում տարր առ տարր գործողություններ մեկ կամ մի քանի զանգվածների վրա։

Ի՞նչ է մնում մշակողների համար: Սա կարելի է մոտավորապես բաժանել չորս մասի. Առաջինը հավելվածի ճարտարապետությունն է, որը պարզապես չի կարող մեզ համար ստեղծել:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Զուգահեռացումը նույնպես բարդ բան է կոմպիլյատորների համար։ Հիշողության հետ աշխատել, քանի որ դա իսկապես դժվար է. պետք է հաշվի առնել ճարտարապետությունը, զուգահեռացումը և ամեն ինչ միասին: Բացի այդ, կոմպիլյատորները չգիտեն, թե ինչպես ճիշտ գնահատել օպտիմալացման որակը, որքան արագ է կոդը: Մենք՝ մշակողներս, նույնպես պետք է դա անենք, որոշում կայացնենք՝ հետագա օպտիմալացնել կամ դադարեցնել:

Ճարտարապետության կողմից մենք կանդրադառնանք վերադիր ծախսերի ամորտիզացիային, վիրտուալ զանգերին, որոնց վրա հիմնված է ճարտարապետության մեծ մասը:

Զուգահեռացումը հավասարումից դուրս թողնենք։ Ինչ վերաբերում է հիշողության օգտագործմանը. Արդյունավետության գնահատման առումով մենք կխոսենք պրոֆիլավորման և կոդի մեջ խոչընդոտներ փնտրելու մասին:

Միջերեսների և վերացական տվյալների տեսակների օգտագործումը նախագծման հիմնարար տեխնիկաներից մեկն է: Եկեք նայենք մեքենայական ուսուցման նմանատիպ հաշվողական կոդը: Սա պայմանական կոդ է, որը թարմացնում է կանխատեսումը գրադիենտ մեթոդով:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Եթե ​​մենք մի փոքր նայենք ներսում և փորձենք հասկանալ, թե ինչ է կատարվում ներսում, մենք ունենք IDerCalcer ինտերֆեյս՝ կորստի ֆունկցիայի ածանցյալները հաշվարկելու համար, և ֆունկցիա, որը փոխում է կանխատեսումը (մեր կանխատեսումը)՝ կորստի ֆունկցիայի գրադիենտին համապատասխան։

Սլայդի աջ կողմում դուք կարող եք տեսնել, թե դա ինչ է նշանակում երկչափ պատյանի համար: Իսկ մեքենայական ուսուցման մեջ կանխատեսման չափը ոչ թե երկու կամ երեք, այլ միլիոնավոր, տասնյակ միլիոնավոր տարրեր է։ Տեսնենք, թե որքան լավ է այս կոդը մոտ 10 միլիոն տարրերից բաղկացած վեկտորի համար:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Եկեք ընդունենք ստանդարտ շեղումը որպես թիրախային ֆունկցիա և չափենք, թե ինչպես է այն աշխատում, որքան ժամանակ կպահանջվի այս կանխատեսումը փոխելու համար: Այս օբյեկտիվ ֆունկցիայի ածանցյալը ցուցադրված է սլայդում: Պայմանական մեքենայի վրա գործող ժամանակը, որն այնուհետև մնում է ֆիքսված, 40 ms է:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Փորձենք հասկանալ, թե ինչն է այստեղ սխալ: Առաջին բանը, որ ուշադրություն է գրավում, վիրտուալ զանգերն են։ Եթե ​​նայեք պրոֆիլավորողին, կարող եք տեսնել, որ կախված պարամետրերի քանակից, սա մոտավորապես հինգից տասը հրահանգ է: Եվ եթե, ինչպես մեր դեպքում, ածանցյալի հաշվարկն ինքնին ընդամենը երկու թվաբանական գործողություն է, ապա դա կարող է հեշտությամբ պարզվել որպես զգալի գերավճար: Մեծ մարմնի համար ածանցյալները հաշվարկելիս սա մոտավորապես կազմում է: Կարճ մարմնի համար, որը հաշվում է ածանցյալը, ասենք, նույնիսկ ոչ թե 500 հրահանգ, այլ 20, 50 կամ նույնիսկ ավելի քիչ, սա արդեն ժամանակի ընթացքում զգալի տոկոս կլինի: Ինչ անել? Փորձենք ամորտիզացնել վիրտուալ ֆունկցիայի կանչը՝ փոխելով ինտերֆեյսը։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Սկզբում մենք հաշվարկում էինք ածանցյալները կետային ուղղությամբ՝ վեկտորի յուրաքանչյուր տարրի համար առանձին: Տարր առ տարր մշակումից անցնենք վեկտորային մշակմանը։ Վերցնենք ստանդարտ C++ ձևանմուշ, որը թույլ է տալիս աշխատել վեկտորի վրա դիտման հետ: Կամ, եթե ձեր կոմպիլյատորը չի աջակցում վերջին ստանդարտին, կարող եք օգտագործել պարզ տնական դաս, որը պահում է տվյալների և չափի ցուցիչը: Ինչպե՞ս կփոխվի ծածկագիրը: Մեզ կմնա մեկ զանգ, որը հաշվարկում է ածանցյալները, այնուհետև մենք պետք է ավելացնենք մի հանգույց, որն իրականում կթարմացնի կանխատեսումը:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Բացի ցիկլ ավելացնելուց, մենք ստիպված կլինենք նաև երկրորդ անգամ դիտարկել տվյալները, այսինքն՝ կարդալ կանխատեսման վեկտորը և գրադիենտ վեկտորը, որը մենք պարզապես երկրորդ անգամ ենք հաշվարկել:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Եկեք նորից փորձենք նույն մեքենայի վրա և տեսնենք, որ ավելի վատ է ստացվել, ինչ-որ բան սխալ է տեղի ունեցել: Եկեք պարզենք, թե ինչ եղավ կոդի հետ:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Ինչ-որ բանի ցիկլը կասկածելու իմաստ չկա, քանի որ սա հենց նույն հաճախականության օրինաչափությունն է, որը կոմպիլյատորները լավ ճանաչում և օպտիմալացնում են: Գործողությունների թիվը մեկ տվյալների տարրի համար ավելի քիչ կլինի, քան վիրտուալ զանգի արժեքը:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Բայց ստեղծելով մեծ վեկտոր և բազմիցս անցնելով դրա միջով, այստեղ պետք է կասկածել խնդրին: Հասկանալու համար, թե ինչու է դա վատ և դանդաղում, դուք պետք է պատկերացնեք, թե ինչ է տեղի ունենում հիշողության մեջ, երբ գործում է այն կոդը, որը մենք տեսնում ենք աջ կողմում գտնվող սլայդում:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Երբ հաշվարկվում է ածանցյալների վեկտորը, այն գալիս է մի օղակի, որը փոխում է կանխատեսումը: Մինչ այս ցիկլը տվյալների միայն շատ փոքր մասը կմնա արագ L1 քեշում, որն աշխատում է պրոցեսորի արագությամբ: Սլայդի վրա այն կանաչ է լուսացույցի վրա: Մնացած տվյալները քեշից դուրս կմղվեն հիշողություն, և երբ ցիկլը սկսի թարմացնել կանխատեսումները, տվյալները պետք է երկրորդ անգամ ընթերցվեն հիշողությունից: Բայց մեզ մոտ դա աշխատում է, ընդհանուր առմամբ, շատ դանդաղ, քայլելու արագությամբ։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Երբ մենք թարմացնում ենք կանխատեսումը, մենք ստիպված չենք լինում միանգամից կարդալ բոլոր ածանցյալները: Վիրտուալ զանգերը կլանելու համար բավական է դրանք հաշվել մեծ փաթեթներով։ Հետևաբար, իմաստ ունի ածանցյալների հաշվարկը և կանխատեսումը թարմացնելը փոքր բլոկների բաժանել և խառնել այս երկու գործողությունները: Ինչի՞ դա կհանգեցնի, եթե նայենք, թե որտեղից են ընթերցվելու տվյալները:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Սա կհանգեցնի նրան, որ մենք անընդհատ տվյալներ ենք վերցնելու, և այն, որ տվյալները կմնան L1 քեշում և ժամանակ չեն ունենա դանդաղ հիշողության մեջ մտնելու համար: Եվ հետո մենք պետք է հասկանանք, թե ով մեզ կասի այս բլոկի չափը:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Տրամաբանական է դա վստահել հենց ածանցյալ հաշվիչին, քանի որ միայն նա գիտի, թե որքան քեշ է իրեն անհրաժեշտ։ Հաջորդը մենք պետք է վերագրենք այն օղակը, որը նայում էր զանգվածի միջով: Այն պետք է բաժանել երկու մասի. Արտաքին օղակը կանցնի բլոկների միջով, իսկ ներսում մենք երկու անգամ կանցնենք բլոկի տարրերով:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Ահա այն, արտաքին բլոկներով:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Եվ ահա ներքինը բլոկի տարրերի համար:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Մենք հաշվի ենք առնում, որ վերջին բլոկը կարող է թերի լինել։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Տեսնենք, թե ինչ է ստացվում սրանից: Մենք տեսնում ենք, որ ճիշտ կռահեցինք, ճիշտ հասկացանք, թե ինչ է կատարվում, և կոդի բավականին փոքր փոփոխությունների գնով ութ տոկոսով կրճատեցինք գործառնական ժամանակը։ Բայց մենք կարող ենք ավելին անել: Պետք է ևս մեկ քննադատական ​​հայացք գցենք մեր գրածին: Նայեք ֆունկցիան, որը մեզ համար հաշվարկում է ածանցյալները: Այն մեզ վերադարձնում է ածանցյալների վեկտոր, որոնց տարրերը դանդաղ հասանելի կլինեն անբարենպաստ իրավիճակներում:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Այստեղ երկու պատճառ կա. Նախ՝ տեղադրելով վեկտորը կույտի վրա։ Հավանականությունը մեծ է, որ այս վեկտորը բազմիցս կստեղծվի և կկործանվի: Արագության առումով երկրորդ թերությունն այն է, որ ամեն անգամ հիշողություն ենք ստանալու, հավանաբար նոր հասցեով։ Այս հիշողությունը «սառը» կլինի քեշի տեսանկյունից, ինչը նշանակում է, որ նախքան դրան գրելը, պրոցեսորը կկատարի օժանդակ ընթերցում՝ քեշի տվյալները սկզբնավորելու համար:

Դա շտկելու համար անհրաժեշտ է բաշխումը դուրս հանել օղակից: Դա անելու համար մենք ստիպված կլինենք կրկին փոխել ինտերֆեյսը, դադարեցնել վեկտորները վերադարձնելը և սկսել ածանցյալներ գրել հիշողության մեջ, որը մենք ստանում ենք կանչող կոդից:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Սա ստանդարտ տեխնիկա է՝ հեռացնելով բոլոր մանիպուլյացիաները ռեսուրսներով հաշվողական կոդի խցաններից: Եկեք ավելացնենք ևս մեկ պարամետր CalcDer մեթոդին, այն վեկտորի տեսքը, որտեղ պետք է գնան ածանցյալները:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Կոդը նույնպես կփոխվի ակնհայտ ձևերով։ Ածանցյալների վեկտորը կլինի մեկ՝ բոլոր օղակներից դուրս, և մեթոդին պարզապես կավելացվի նոր պարամետր։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Եկեք տեսնենք. Ստացվում է, որ նախորդ տարբերակի համեմատ մոտ ութ տոկոսով ավելի ենք շահել, իսկ բազային տարբերակի համեմատ՝ արդեն 15 տոկոս։

Հասկանալի է, որ օպտիմալացումները չեն սահմանափակվում վերադիր ծախսերի ամորտիզացիայով, որ կան նաև այլ տեսակի խցանումներ։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Պատկերացնելու համար, թե ինչպես փնտրել խցանումներ, մեզ անհրաժեշտ է ևս մեկ պարզ թեստային ծածկագիր: Օրինակ, ես վերցրեցի մատրիցային տրանսպոսը: Մենք ունենք մոտավորապես մատրիցա և approxByCol մատրիցա, որտեղ մենք պետք է տեղադրենք փոխադրված տվյալները: Եվ երկու օղակների պարզ բույն: Այստեղ չկան վիրտուալ զանգեր կամ վեկտորի ստեղծում: Դա պարզապես տվյալների փոխանցում է: Օղակը համեմատաբար հարմար է կոմպիլյատորների համար:

Եկեք չափենք, թե ինչպես է այս կոդը աշխատում բավականին մեծ մատրիցայի և կոնկրետ մեքենայի վրա:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Օրինակ՝ ես տողերի թիվը վերցրեցի 1000, սյունակների թիվը՝ 100 Մեքենան Intel սերվեր է, մեկ միջուկ։ Հիշողությունը հենց այսպիսին է, սա մեզ համար կարևոր է, քանի որ հիշողության և արագության հետ աշխատանքը կախված կլինի հիշողության արագությունից: Չափեցինք և ստացանք 000 վ. Շա՞տ է, թե՞ քիչ։ Ի՞նչ է մեզ հաջողվում անել այս ընթացքում։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Մեզ հաջողվում է կարդալ 800 մեգաբայթ, սա ոչ թե տրանսպոզիցիոն մատրիցա է, այլ բնօրինակը։ Եվ նաև կարդացեք և գրեք 1,6 ԳԲ, սա արդեն փոխադրված մատրիցա է: Պրոցեսորը կատարում է օժանդակ ընթերցում գրելուց առաջ՝ քեշի տվյալները սկզբնավորելու համար:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Եկեք հաշվարկենք, թե որքան թողունակություն ենք մենք օգտագործել օգտակար: Պարզվում է, որ մեր կոդի թողունակությունը եղել է 1,7 ԳԲ/վ։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Սա տեսական հաշվարկ էր։ Հաջորդը, դուք կարող եք օգտագործել պրոֆիլավորող, որը կարող է չափել հիշողության հետ աշխատելու արագությունը: Ես վերցրեցի VTune-ը: Տեսնենք, թե ինչ է նա ցույց տալիս։ Ցույց է տալիս նմանատիպ ցուցանիշ՝ 1,8 ԳԲ: Սկզբունքորեն դա լավ է համաձայնվում, քանի որ մեր հաշվարկում մենք հաշվի չենք առել, որ պետք է կարդալ տողերի հասցեները և սյունակների հասցեները։ Բացի այդ, VTune-ը գրանցում է ֆոնային գործունեությունը օպերացիոն համակարգում: Հետեւաբար, մեր մոդելը համապատասխանում է իրականությանը։

Որպեսզի հասկանանք՝ 1,7 ԳԲ-ը շատ է, թե քիչ, մենք պետք է հասկանանք, թե ինչ առավելագույն թողունակություն է մեզ հասանելի:

Դա անելու համար հարկավոր է կարդալ պրոցեսորի բնութագրերը: Կա ark.intel.com հատուկ կայք, որտեղ կարելի է ամեն ինչ իմանալ ցանկացած պրոցեսորի մասին։ Եթե ​​հատուկ նայենք մեր սերվերին, ապա կտեսնենք, որ այն ունի ութ միջուկ, և ամենաարագ DDR3 հիշողությունը, որն այն աջակցում է, կարող է փոխանցվել մոտավորապես 60 ԳԲ/վ արագությամբ մեկ ուղղությամբ:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Բայց այստեղ պետք է հաշվի առնել, որ մենք օգտագործում ենք միայն մեկ միջուկ, և մեր հիշողությունն ավելի դանդաղ է, այսինքն՝ պետք է այս 60 ԳԲ-ը չափել մեր պայմաններին միջուկների քանակին և հիշողության հաճախականությանը համամասնորեն։

Պարզվում է, որ մեր կոդը կարող էր օգտագործել 5,3 ԳԲ միակողմանի: Եվ քանի որ դուք կարող եք կարդալ և գրել զուգահեռաբար, իդեալականը, եթե մենք պարզապես պատճենեինք տվյալները տեղից տեղ, մենք կհասնեինք 10,6: Քանի որ մենք ունենք երկու ընթերցում և մեկ գրություն, այն պետք է լինի մոտավորապես 8 ԳԲ/վ: Հիշում ենք, որ ստացանք 1,7։ Այսինքն՝ մենք օգտագործել ենք մոտ 20 տոկոս։

Ինչու է դա տեղի ունենում: Կրկին պետք է հասկանալ ճարտարապետությունը: Փաստն այն է, որ հիշողության և քեշի միջև տվյալները փոխանցվում են ոչ թե կամայական փաթեթներով, այլ ուղիղ 64 բայթով, ոչ ավել, ոչ պակաս: Սա առաջին նկատառումն է։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Երկրորդ նկատառումն այն է, որ մենք փոխադրված տվյալները գրում ենք ոչ թե հաջորդական, այլ պատահական, քանի որ մատրիցայի տողերը գտնվում են հիշողության մեջ անկանխատեսելի կերպով:

Ստացվում է, որ մեկ իրական թիվ գրելուց առաջ պետք է կարդալ 64 բայթ տվյալ։ Եթե ​​մատրիցայի չափը նշանակում ենք N, ապա օպտիմալ գործառնական ժամանակի փոխարեն (N/5,3 + N/10,6) ստանում ենք (8*N/5,3 + N/10,6): Ինչ-որ տեղ չորս-հինգ անգամ ավելի շատ, ինչը բացատրում է այս 20% արդյունավետությունը:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Ի՞նչ անել դրա հետ կապված: Դուք պետք է դադարեցնեք տվյալների գրելը մեկ սյունակում և սկսեք գրել այնքան սյունակ, որքան տեղավորվում է մեկ քեշի տողում (64 բայթ): Դա անելու համար մենք սյունակների երկայնքով օղակը կբաժանենք մի օղակի քեշի գծերի վրայով և կցված հանգույցի քեշի գծի տարրերի վրա:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Ահա դրանք, կրկնություններ քեշի գծերի երկայնքով:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Եվ ահա դրանք, կրկնություններ քեշի գծի ներսում: Այստեղ, պարզության համար, մենք ենթադրում ենք, որ տվյալները հավասարեցված են քեշի գծի սահմանին: Հիմա եկեք ստուգենք, թե ինչ է տեղի ունենում VTune-ի միջոցով:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Մենք տեսնում ենք, որ արդյունքը մոտ է վայրկյանում հաշվարկված ութ գիգաբայթին՝ 7,6։ Բայց փաստ չէ, որ այս բոլոր 7,6-ը օգտակար աշխատանք է։ Միգուցե դրանցից մի քանիսը գլխավերեւում են:

Հասկանալու համար, թե որքան օգուտ ենք ստացել, եկեք չափենք օպտիմիզացումից հետո գործառնական ժամանակը: Նույն մեքենայի վրա ստացվում է 0,5 վ. Ինքնին փոխադրման շնորհիվ թողունակությունը դարձել է 4,8 ԳԲ/վ: Երևում է, որ դեռ ռեզերվ կա, որը մենք չենք ընտրել, բայց, այնուամենայնիվ, 20 տոկոս արդյունավետությունից ստացել ենք 60 տոկոս։

Օգտագործելով պրոֆիլը, մենք կարող ենք պարզել, թե ինչու մենք չենք ստացել 80% կամ 95%:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Փաստն այն է, որ մենք մատրիցները պահում ենք որպես վեկտորների վեկտոր, այսինքն՝ օգտագործում ենք հիշողության հասանելիությունը կրկնակի անուղղությամբ։

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Օգտագործելով VTune-ը, դուք կարող եք տեսնել, թե որ հրահանգներն են ստեղծվում զանգվածի տարրեր մուտք գործելու համար: Հրահանգները, որոնք կարդում են փոխադրված մատրիցայի սյունակների հասցեները, ընդգծված են ձախ կողմում դեղին գույնով: Եվ դրանք, առաջին հերթին, լրացուցիչ հրահանգներ են, երկրորդը, լրացուցիչ տվյալների փոխանցումներ: Բայց մենք հետագա օպտիմալացում չենք անի, կանգ առնենք և ամփոփենք:

C++ օպտիմիզացում՝ միավորել արագությունը և բարձր մակարդակը: Յանդեքսի հաշվետվություն

Ինչի՞ մասին էի պատմել քեզ այսօր։ Հաշվողական կոդի հետ աշխատելու համար օգտակար հուշում է բլոկներով մշակելը՝ ամորտիզացնելով, օրինակ, վիրտուալ զանգերի հետ կապված ծախսերը: Բացի այդ, արգելափակման պատճառով տվյալների տեղայնությունը բարելավվում է, և մենք ստանում ենք մուտքի ավելի բարձր արագություն:

Հատկացումները փակուղիներից հանելը նաև դրանց ամորտիզացիան է։ Եվ նաև ավելացնելով մուտքի արագությունը՝ հիշողության մեջ ժամանակավոր բուֆերների ամրագրմամբ:

Ինչ վերաբերում է պրոֆիլավորմանը. Նախ, պրոֆիլավորումը օգտակար տեխնիկա է «ընդհանուր առմամբ» խոչընդոտներ գտնելու համար: Երկրորդ, այն մեզ թույլ է տալիս գնահատել կոդի արդյունավետությունը, որոշել՝ գոհ ենք արագությունից, թե ցանկանում ենք հետագա օպտիմալացում կատարել, և ցույց է տալիս, թե որ ուղղությամբ շարժվել։

Ինձ համար այսքանն է: Եթե ​​օգտագործում եք CatBoost կամ առաջին անգամ եք լսել դրա մասին և ցանկանում եք իմանալ, թե ինչ է դա, կարդացեք հոդվածներ Habré-ի մասին, համեցե՛ք մեզ մոտ GitHub, գրեք հեռագիր. Շատ շնորհակալ եմ ուշադրության համար։

Source: www.habr.com