OpenCV: цикл по всем пик­се­лям изоб­ра­же­ния и сов­ме­ще­ние ука­за­те­лей

За­ча­стую мы про­грам­ми­ру­ем «как удоб­но», и счи­та­ем, что это за­да­ча ком­пи­ля­то­ра — ге­не­ри­ро­вать эф­фек­тив­ный код. При этом мы за­бы­ва­ем, на­сколь­ко тя­жёл труд ком­пи­ля­то­ра. Ведь он дол­жен ге­не­ри­ро­вать в первую оче­редь кор­рект­ный код, и уже во вто­рую оче­редь — быст­рый. По­это­му мно­гие оп­ти­ми­за­ции ком­пи­ля­тор при­ме­ня­ет очень осто­рож­но, и от­клю­ча­ет их при ма­лей­шем по­до­зре­нии на не­при­ме­ни­мость. В свя­зи с этим ком­пи­ля­то­ру нуж­но по­мо­гать, тем бо­лее, что ино­гда это сде­лать со­всем про­сто.

Рас­смот­рим за­да­чу об­хо­да всех пик­се­лей изоб­ра­же­ния cv::Mat ти­па CV_8UC1 (один unsigned char на пик­сель) на при­ме­ре ин­вер­ти­ро­ва­ния цве­тов. Ком­пи­ли­ро­вать бу­дем в Visual Studio 2010 с оп­ци­ей оп­ти­ми­за­ции /O2.

Пер­вая по­пыт­ка: Mat::at

Ти­пич­ная реа­ли­за­ция вы­гля­дит сле­дую­щим об­ра­зом:

void invert(cv::Mat &image) //image.type() == CV_8UC1
{
    for(int y(0); y < image.rows; ++y)
    {
        for(int x(0); x < image.cols; ++x)
            image.at<unsigned char>(y, x) = 255 - image.at<unsigned char>(y, x);
    }
}

Ли­стинг 1. Про­стая реа­ли­за­ция ин­вер­ти­ро­ва­ния од­но­ка­наль­но­го изоб­ра­же­ния в OpenCV

Срав­не­ние вре­мён ра­бо­ты раз­лич­ных вер­сий ко­да при­ве­де­но в кон­це ста­тьи.

К че­му здесь мож­но при­драть­ся? Кто-то мо­жет ска­зать, что на каж­дой ите­ра­ции цик­ла про­ис­хо­дит дву­крат­ный вы­зов функ­ции cv::Mat::at<>(), и же­ла­тель­но за­пом­нить ссыл­ку, воз­вра­ща­е­мую этой функ­ци­ей, во вре­мен­ную пе­ре­мен­ную, что­бы обой­тись без дву­крат­но­го вы­зо­ва. На са­мом де­ле вы­зо­вов функ­ции at по­сле ком­пи­ля­ции не оста­нет­ся. Эти функ­ции встраи­ва­ют­ся в вы­зы­ваю­щий код (inline expansion), а дву­крат­ное вы­чис­ле­ние од­но­го и то­го же ад­ре­са ком­пи­ля­тор за­ме­ня­ет на од­но­крат­ное вы­чис­ле­ние.

Вот так вы­гля­дит ас­сем­блер­ный код внут­рен­не­го цик­ла, вы­да­вае­мый ком­пи­ля­то­ром (здесь и да­лее я опус­каю на­чаль­ную про­вер­ку воз­мож­но­сти вы­пол­нить хо­тя бы од­ну ите­ра­цию, ком­мен­та­рии к ко­ду мои):

$LL3@invert:
//Вычисление адреса пикселя unsigned char *p (функция at)
mov edx, DWORD PTR [eax+44]
mov edx, DWORD PTR [edx]
imul edx, edi
add edx, DWORD PTR [eax+16]
lea esi, DWORD PTR [edx+ecx]

or dl, 255             //dl = 255
sub dl, BYTE PTR [esi] //dl -= *p
inc ecx                //++x
mov BYTE PTR [esi], dl //*p = dl

mov edx, DWORD PTR [eax+12] //edx = image.cols
cmp ecx, edx                //x < edx ?
jl SHORT $LL3@invert        //Если да, то повторяемм цикл

Ли­стинг 2. Ас­сем­блер­ный код, сге­не­ри­ро­ван­ный ком­пи­ля­то­ром Visual Studio 2010
для внут­рен­не­го цик­ла про­грам­мы ли­стин­га 1

Для на­гляд­но­сти я раз­бил ас­сем­блер­ные ин­ст­рук­ции на 3 бло­ка:

  1. вы­чис­ле­ние ад­ре­са пик­се­ля (функ­ция at), на­зо­вём этот ад­рес unsigned char *p;
  2. мо­дифи­ка­ция пик­се­ля *p = 255 - *p;
  3. на­ра­щи­ва­ние счёт­чи­ка цик­ла и про­вер­ка усло­вия вы­хо­да из цик­ла.

Ин­те­рес­но, что ин­ст­рук­ция inc ecx (на­ра­щи­ва­ние счёт­чи­ка цик­ла) на са­мом де­ле при­над­ле­жит тре­тье­му бло­ку. Ком­пи­ля­тор по­ста­вил эту ин­ст­рук­цию в се­ре­ди­ну вы­чис­ле­ния вы­ра­же­ния *p = 255 - *p, ви­ди­мо, для то­го, что­бы чем-то за­нять про­цес­сор на слу­чай про­ма­ха кэ­ша при вы­пол­не­нии чте­ния па­мя­ти sub dl, BYTE PTR [esi].

Итак, ка­кие здесь про­бле­мы? В гла­за бро­са­ют­ся 6 об­ра­ще­ний к па­мя­ти за од­ну ите­ра­цию цик­ла, 3 из ко­то­рых при­хо­дят­ся на функ­цию at (опе­ра­ция lea esi, DWORD PTR [edx+ecx] об­ра­ще­ни­ем к па­мя­ти не яв­ля­ет­ся; это про­сто из­вра­щён­ный спо­соб за­пи­сать в ре­гистр сум­му двух дру­гих ре­ги­стров). Мы зна­ем, что об­ра­ще­ние к па­мя­ти — наи­боль­шее зло, ибо оно мо­жет при­ве­сти к про­ма­ху кэ­ша и про­стою кон­вейе­ра.

Что­бы по­нять при­чи­ну об­ра­ще­ний, посмот­рим на ис­ход­ный код функ­ции at (я убрал ужас­ный assert, ко­то­рый там был):

template<typename _Tp> inline _Tp& Mat::at(int i0, int i1)
{
    return ((_Tp*)(data + step.p[0]*i0))[i1];
}

Ли­стинг 3. Ис­ход­ный код ис­поль­зу­е­мо­го ва­ри­ан­та функ­ции Mat::at

В прин­ци­пе, всё по­нят­но: к ад­ре­су пер­во­го пик­се­ля изоб­ра­же­ния при­бав­ля­ет­ся дли­на стро­ки в бай­тах, умно­жен­ная на но­мер стро­ки, и но­мер столб­ца, умно­жен­ный на раз­мер пик­се­ля. По­след­нее умно­же­ние вы­пол­ня­ет­ся опе­ра­ци­ей ин­дек­са­ции и в на­шем слу­чае от­сут­ству­ет в ас­сем­блер­ном ко­де, так как раз­мер пик­се­ля ра­вен еди­ни­це (unsigned char).

Наш враг — сов­ме­ще­ние ука­за­те­лей

Но за­чем каж­дый раз пе­ре­чи­ты­вать все эти чис­ла из па­мя­ти? Ведь не бу­дет же, на­при­мер, ши­ри­на изоб­ра­же­ния ме­нять­ся по ме­ре его ин­вер­ти­ро­ва­ния! Или бу­дет?

На са­мом де­ле по ме­ре об­ра­бот­ки изоб­ра­же­ния его ши­ри­на (и дру­гие па­ра­мет­ры) мо­гут из­ме­нить­ся! Для это­го до­ста­точ­но пе­ред вы­зо­вом функ­ции invert рас­по­ло­жить бу­фер па­мя­ти изоб­ра­же­ния по­верх ст­рук­ту­ры cv::Mat &image (ад­рес бу­фе­ра хра­нит­ся в по­ле cv::Mat::data ти­па uchar*). То есть ши­ри­на и вы­со­та об­ра­ба­ты­ва­е­мо­го изоб­ра­же­ния, ад­рес его пер­во­го пик­се­ля, дли­на стро­ки (не все­гда рав­на ши­ри­не), и дру­гие па­ра­мет­ры мо­гут яв­лять­ся пик­се­ля­ми это­го са­мо­го изоб­ра­же­ния и, зна­чит, из­ме­нить­ся при его ин­вер­ти­ро­ва­нии. О та­ком не­ве­ро­ят­ном сце­на­рии мы да­же по­ду­мать не мог­ли, а за­бот­ли­вый ком­пи­ля­тор всё преду­смот­рел и сге­не­ри­ро­вал ужас­но не­эф­фек­тив­ный, но кор­рект­ный код.

По по­во­ду сов­ме­ще­ния ука­за­те­лей смот­ри­те так­же эту ста­тью на рус­ском язы­ке.

Яв­ле­ние, ко­то­рое я опи­сал, на­зы­ва­ет­ся сов­ме­ще­ни­ем ука­за­те­лей (pointer aliasing). Воз­мож­ность та­ко­го сов­ме­ще­ния — од­на из ос­нов­ных про­блем, ме­шаю­щих ком­пи­ля­то­ру про­из­во­дить оп­ти­ми­за­ции. Что­бы убе­дить­ся, что имен­но сов­ме­ще­ние ука­за­те­лей яв­ля­ет­ся при­чи­ной пло­хо­го ко­да, рас­смот­рим функ­цию, не про­из­во­дя­щую мо­дифи­ка­цию изоб­ра­же­ния:

int sum(cv::Mat image) //image.type() == CV_8UC1
{
    int sum(0);
    for(int y(0); y < image.rows; ++y)
    {
        for(int x(0); x < image.cols; ++x)
            sum += image.at<unsigned char>(y, x);
    }
    return sum;
}

Ли­стинг 4. Мо­дифи­ка­ция изоб­ра­же­ния убра­на.
Об­ра­ти­те вни­ма­ние на то, что изоб­ра­же­ние пе­ре­да­ёт­ся не по ссыл­ке

$LL3@invert:
movzx ebx, BYTE PTR [ecx+eax]
inc eax
add esi, ebx
cmp eax, edx
jl SHORT $LL3@invert

Ли­стинг 5. Ас­сем­блер­ный код, сге­не­ри­ро­ван­ный ком­пи­ля­то­ром
для внут­рен­не­го цик­ла ли­стин­га 4. Срав­ни­те с ли­стин­гом 2

Кру­то, не прав­да ли? От цик­ла остал­ся лишь не­об­хо­ди­мый ми­ни­мум ин­ст­рук­ций: един­ствен­ное об­ра­ще­ние к па­мя­ти, сум­ми­ро­ва­ние, на­ра­щи­ва­ние счёт­чи­ка цик­ла, и про­вер­ка за­вер­ше­ния цик­ла.

От­ве­чаю на на­зрев­ший у вас во­прос: пе­ре­да­ча изоб­ра­же­ния не по ссыл­ке в ко­де из ли­стин­га 1 по­чти не ме­ня­ет ас­сем­блер­ный код внут­рен­не­го цик­ла: там про­сто не­мно­го из­ме­нит­ся ад­ре­са­ция.

Но что­бы по­лу­чить от ком­пи­ля­то­ра столь эф­фек­тив­ный код, при­шлось по­жерт­во­вать пе­ре­да­чей cv::Mat по ссыл­ке. Не­смот­ря на то, что cv::Mat осна­щён счёт­чи­ком ссылок, и ко­пи­ро­ва­ние объ­ек­та это­го клас­са не озна­ча­ет ко­пи­ро­ва­ние его дан­ных, на­клад­ные рас­хо­ды всё ещё су­ще­ствен­ны, и при­мер­но рав­ны вре­ме­ни об­ра­бот­ки (сум­ми­ро­ва­ния) мат­ри­цы 100×100 пик­се­лей. По­пыт­ка пе­ре­дать объ­ект по ссыл­ке (пусть да­же кон­стант­ной) при­ве­дёт к ге­не­ра­ции ко­да, ана­ло­гич­но­го ли­стин­гу 2. Ви­ди­мо, ком­пи­ля­тор опа­са­ет­ся, что па­ра­мет­ры объ­ек­та бу­дут из­ме­не­ны извне во вре­мя его об­ра­бот­ки.

Стро­гое сов­ме­ще­ние ука­за­те­лей

Мож­но по­ду­мать, что про­бле­мы оп­ти­ми­за­ции, свя­зан­ные с сов­ме­ще­ни­ем ука­за­те­лей, ушли с вве­де­ни­ем стро­го­го сов­ме­ще­ния ука­за­те­лей (strict pointer aliasing) в но­вых стан­дар­тах Си (C99) и Си++ (C++03). Вкрат­це, стро­гое сов­ме­ще­ние ука­за­те­лей — это ко­гда толь­ко ука­за­те­ли од­но­го ти­па ком­пи­ля­тор счи­та­ет по­до­зри­тель­ны­ми на сов­ме­ще­ние. Ис­клю­че­ние — ука­затель на char, ко­то­ро­му раз­ре­ше­но на­кла­ды­вать­ся на дру­гие ука­за­те­ли.

Си­ту­а­ция на­по­ми­на­ет си­ту­а­цию с ку­чей кри­вых сай­тов, сво­им су­ще­ство­ва­ни­ем сдер­жи­ваю­щих раз­ви­тие бра­у­зе­ров.

Од­на­ко боль­шое ко­ли­че­ство ко­да (осо­бен­но низ­ко­уров­не­во­го) за­ви­сит от воз­мож­но­сти не­стро­го­го сов­ме­ще­ния. Вся биб­лио­те­ка OpenCV про­сто рас­сы­пет­ся при ком­пи­ля­ции с вклю­чён­ным стро­гим сов­ме­ще­ни­ем, имен­но по­это­му её нуж­но ком­пи­ли­ро­вать с клю­чём -fno-strict-aliasing (при ис­поль­зо­ва­нии GCC). К сча­стью для поль­зо­ва­те­лей Windows, ком­пи­ля­тор Visual Studio не под­дер­жи­ва­ет strict aliasing, по­это­му от­клю­чать его не при­хо­дит­ся.

По­это­му бу­дем ис­кать дру­гие пу­ти по­лу­че­ния оп­ти­маль­но­го ко­да, от­лич­ные от вклю­че­ния strict aliasing в ком­пи­ля­то­ре.

Ис­поль­зу­ем Mat::ptr

Ещё раз озву­чу вы­яс­нен­ную на­ми при­чи­ну ге­не­ра­ции не­опти­маль­но­го ко­да (ли­стинг 2): ком­пи­ля­тор опа­са­ет­ся, что не­ко­то­рые па­ра­мет­ры изоб­ра­же­ния из­ме­нят­ся во вре­мя ра­бо­ты внут­рен­не­го цик­ла, из-за че­го по­сто­ян­но пе­ре­чи­ты­ва­ет их из па­мя­ти, что, в свою оче­редь, ве­дёт к не­воз­мож­но­сти вы­но­са по­вто­ряю­щих­ся вы­чис­ле­ний (на­при­мер, умно­же­ния но­ме­ра те­ку­щей стро­ки на её раз­мер) за пре­де­лы внут­рен­не­го цик­ла, так как ре­зуль­тат этих вы­чис­ле­ний мо­жет (по мне­нию ком­пи­ля­то­ра) в лю­бой мо­мент из­ме­нить­ся.

Те­перь, ко­гда из­ве­ст­на при­чи­на, с ней мож­но бо­роть­ся. Для это­го нуж­но ско­пи­ро­вать ча­сто ис­поль­зуе­мые во внут­рен­нем цик­ле дан­ные в ло­каль­ные пе­ре­мен­ные. Эти пе­ре­мен­ные, бу­дучи со­зда­ны по­сле ис­поль­зу­е­мо­го ука­за­те­ля (ука­за­те­ля на дан­ные изоб­ра­же­ния cv::Mat::data), оче­вид­но яв­ля­ют­ся от ука­за­те­ля не­за­ви­си­мы­ми (с точ­ки зре­ния ком­пи­ля­то­ра), и сов­ме­ще­ние не­воз­мож­но.

Ес­ли посмот­реть ис­ход­ный код функ­ции Mat::at (ли­стинг 3), то по­лу­ча­ет­ся, что нам на­до со­хра­нить в ло­каль­ные пе­ре­мен­ные зна­че­ния image.data и image.step.p[0], а сам ис­ход­ный код вста­вить в те­ло цик­ла. В об­щем, это пло­хое ре­ше­ние.

К сча­стью, есть го­раз­до бо­лее под­хо­дя­щая функ­ция Mat::ptr, ко­то­рую мож­но ис­поль­зо­вать за пре­де­ла­ми внут­рен­не­го цик­ла (и, зна­чит, мож­но не вол­но­вать­ся об эф­фек­тив­но­сти ге­не­ри­ру­е­мо­го для неё ко­да). Эта функ­ция воз­вра­ща­ет ука­затель на пер­вый пик­сель нуж­ной нам стро­ки:

void invert(cv::Mat image) //image.type() == CV_8UC1
{
    for(int y(0); y < image.rows; ++y)
    {
        unsigned char *const scanLine( image.ptr<unsigned char>(y) );

        for(int x(0); x < image.cols; ++x)
            scanLine[x] = 255 - scanLine[x];
    }
}

Ли­стинг 6. Ис­поль­зу­ем функ­цию ptr

$LL3@invert:
or  dl, 255
sub dl, BYTE PTR [eax+ecx]
inc eax
mov BYTE PTR [eax+ecx-1], dl
mov edx, DWORD PTR [esi+12]
cmp eax, edx
jl  SHORT $LL3@invert

Ли­стинг 7. Ас­сем­блер­ный код внут­рен­не­го цик­ла из ли­стин­га 6

Об­ра­ти­те вни­ма­ние на то, как на­стой­чи­во ком­пи­ля­тор пы­та­ет­ся вста­вить на­ра­щи­ва­ние счёт­чи­ка цик­ла до за­пи­си ре­зуль­ти­рую­ще­го зна­че­ния. Это да­же при­во­дит к то­му, что ком­пи­ля­тор вы­нуж­ден об­рат­но от­нять при­бав­лен­ную еди­ни­цу при за­пи­си в па­мять: mov BYTE PTR [eax+ecx-1], dl. Тем не ме­нее, этот код бо­лее эф­фек­ти­вен, чем ес­ли бы на­ра­щи­ва­ние счёт­чи­ка бы­ло по­сле за­пи­си в па­мять, так как эко­но­мит­ся один такт при про­ма­хе кэ­ша.

Оста­лось од­но лиш­нее чте­ние па­мя­ти для ши­ри­ны изоб­ра­же­ния. Из­ба­вим­ся от не­го, со­хра­няя раз­ме­ры в ло­каль­ных пе­ре­мен­ных:

void invert(cv::Mat &image) //image.type() == CV_8UC1
{
    int const imageWidth(image.cols), imageHeight(image.rows);
   
    for(int y(0); y < imageHeight; ++y)
    {
        unsigned char *const scanLine( image.ptr<unsigned char>(y) );

        for(int x(0); x < imageWidth; ++x)
            scanLine[x] = 255 - scanLine[x];
    }
}

Ли­стинг 8. Раз­ме­ры изоб­ра­же­ния со­хра­не­ны в ло­каль­ных пе­ре­мен­ных

$LL3@invert:
or  bl, 255
sub bl, BYTE PTR [ecx+eax]
inc ecx
mov BYTE PTR [ecx+eax-1], bl
cmp ecx, esi
jl  SHORT $LL3@invert

Ли­стинг 9. Ас­сем­блер­ный код внут­рен­не­го цик­ла из ли­стин­га 8.
По срав­не­нию с ли­стин­гом 7 ис­чез­ла опе­ра­ция чте­ния ши­ри­ны изоб­ра­же­ния из па­мя­ти

Мы ви­дим, что ком­пи­ля­тор со­хра­нил ши­ри­ну изоб­ра­же­ния в ре­ги­стре esi, и не чи­та­ет её каж­дый раз из па­мя­ти.

Мож­но ли вы­жать из это­го ко­да ещё что-ни­будь? Ко­неч­но! За­ме­тим, что ес­ли кру­тить цикл не по воз­рас­та­нию, а по убы­ва­нию, то цикл все­гда бу­дет ид­ти до ну­ля, и ком­пи­ля­то­ру во­об­ще не на­до бу­дет ис­поль­зо­вать cmp для про­вер­ки усло­вия оста­нов­ки, так как каж­дая ариф­ме­ти­че­ская опе­ра­ция со­хра­ня­ет во фла­го­вых ре­ги­страх про­цес­со­ра знак сво­е­го ре­зуль­та­та. Со­от­вет­ствен­но, в этом слу­чае не нуж­но со­хра­нять раз­ме­ры изоб­ра­же­ния в ло­каль­ных пе­ре­мен­ных.

Кро­ме то­го, од­на опе­ра­ция ис­поль­зу­ет­ся для за­груз­ки чис­ла 255 в ре­гистр bl. Ес­ли вме­сто вы­чи­та­ния ис­поль­зо­вать би­то­вое от­ри­ца­ние (стран­но, что ком­пи­ля­тор сам не до­га­дал­ся об этом), то мож­но сэко­но­мить ещё од­ну опе­ра­цию. В ито­ге по­лу­ча­ем:

void invert(cv::Mat &image) //image.type() == CV_8UC1
{
    for(int y(image.rows - 1); y >= 0; --y)
    {
        unsigned char *const scanLine( image.ptr<unsigned char>(y) );

        for(int x(image.cols - 1); x >= 0 ; --x)
            scanLine[x] = ~scanLine[x];
    }
}

Ли­стинг 10. На­прав­ле­ния хо­да цик­лов из­ме­не­ны.
Вме­сто вы­чи­та­ния при­ме­не­но би­то­вое от­ри­ца­ние

$LL3@invert:
dec ecx
mov dl, BYTE PTR [ecx+eax+1]
not dl
mov BYTE PTR [ecx+eax+1], dl
jns SHORT $LL3@invert

Ли­стинг 11. Ас­сем­блер­ный код внут­рен­не­го цик­ла из ли­стин­га 10

Об­ра­ти­те вни­ма­ние, что умень­ше­ние счёт­чи­ка цик­ла ком­пи­ля­тор по­ста­вил ещё рань­ше, и те­перь обе опе­ра­ции об­ра­ще­ния к па­мя­ти име­ют +1 для ком­пен­са­ции это­го преж­де­вре­мен­но­го вы­чи­та­ния.

Под­ве­дём ито­ги. Код из ли­стин­га 1 мы мо­дифи­ци­ро­ва­ли, по­лу­чив код из ли­стин­га 11. При этом в но­вом ко­де все­го на од­ну строч­ку боль­ше, и од­на из стро­чек су­ще­ствен­но ко­ро­че, так что нель­зя од­но­знач­но ска­зать, ка­кой из ва­ри­ан­тов бо­лее сло­жен. В ре­зуль­та­те вме­сто 12-ти ас­сем­блер­ных ин­ст­рук­ций, со­дер­жа­щих 6 об­ра­ще­ний к па­мя­ти, мы по­лу­чи­ли 5 ин­ст­рук­ций и 2 об­ра­ще­ния к па­мя­ти.

Я счи­таю, что по­след­ний ва­ри­ант — са­мый эф­фек­тив­ный из чи­тае­мых ва­ри­ан­тов реа­ли­за­ции. Мож­но, ко­неч­но, про­дви­нуть­ся даль­ше, за­гру­жать бай­ты по 4 шту­ки в 32-х-бит­ную пе­ре­мен­ную, мож­но по­пы­тать­ся ис­поль­зо­вать су­пер­ска­ляр­ные рас­ши­ре­ния, не­сколь­ко ядер про­цес­со­ра и про­чие пре­ле­сти со­вре­мен­ных ком­пью­те­ров. Но всё это от­дель­ная ра­бо­та, тре­бую­щая до­пол­ни­тель­но­го вре­ме­ни, а нам нуж­но де­лать боль­шой про­ект, в ко­то­ром на­хож­де­ние не­га­тив­но­го изоб­ра­же­ния (или что-то по­доб­ное) — лишь мел­кая под­за­да­ча, на ре­ше­ние ко­то­рой у нас име­ет­ся не бо­лее пя­ти ми­нут.

И, на­по­сле­док, не удер­жусь, при­ве­ду ва­ри­ант с ите­ра­то­ром:

void invert(cv::Mat &image) //image.type() == CV_8UC1
{
    for(cv::MatIterator_<unsigned char> i( image.begin<unsigned char>() );
    i != image.end<unsigned char>(); ++i)
        *i = ~*i;
}

Ли­стинг 12. Об­ход мат­ри­цы при по­мо­щи ите­ра­то­ра

Это­му изящ­но­му ко­ду со­от­вет­ству­ет бо­лее сот­ни ас­сем­блер­ных ин­ст­рук­ций, по­это­му не при­во­жу их здесь.

Срав­не­ние вре­ме­ни ра­бо­ты раз­лич­ных вер­сий ко­да

Мой про­цес­сор: Intel Core i5 M 460 2.53GHz, 2 яд­ра + Hyper-threading. Код за­пус­кал­ся на од­ном яд­ре для изоб­ра­же­ний 1024×1024 пик­се­ля (1 ме­га­байт) и 8192×8192 пик­се­ля (64 ме­га­бай­та).

Ва­ри­ант ко­даВре­мя ра­бо­ты (сек)
1024×1024 пик­се­ля
Вре­мя ра­бо­ты (сек)
8192×8192 пик­се­ля
1Mat::at (ли­стинг 1)0.002150.146
2Mat::ptr (ли­стинг 6)0.001310.089
3Раз­ме­ры в пе­ре­мен­ных (ли­стинг 8)0.001300.088
4Цик­лы по убы­ва­нию (ли­стинг 10)0.001300.087
5Ите­ра­тор (ли­стинг 12)0.02111.36

Таб­ли­ца 1. Вре­ме­на ра­бо­ты раз­лич­ных вер­сий ко­да. Мень­ше — луч­ше

При­ят­но осо­зна­вать, что тот ва­ри­ант (но­мер 3), ко­то­рый я ин­ту­и­тив­но вы­брал пол­то­ра го­да на­зад, ко­гда на­чал ис­поль­зо­вать OpenCV в сво­ей де­я­тель­но­сти, те­перь по­лу­чил обос­но­ва­ние. Я люб­лю скан­лай­ны со вре­мён Delphi. Там был ме­тод ScanLine у TBitmap.

Мы ви­дим, что об­ход мат­ри­цы при по­мо­щи Mat::ptr при­мер­но в 1.6 раз быст­рее, чем при по­мо­щи Mat::at. Даль­ней­шие оп­ти­ми­за­ции, не­смот­ря на оче­вид­ное со­кра­ще­ние объ­ё­ма ис­пол­ня­е­мо­го ко­да, к ро­сту про­из­во­ди­тель­но­сти не при­ве­ли. Ви­ди­мо, су­пер­ска­ляр­ность про­цес­со­ра по­ела всю не­эф­фек­тив­ность ко­да. Ва­ри­ант с ите­ра­то­ром ока­зал­ся в 16 раз мед­лен­нее са­мо­го быст­ро­го ва­ри­ан­та, по­это­му его ис­поль­зо­вать не сле­ду­ет.

За­клю­чи­тель­ные со­ве­ты

  • Посмот­ри­те, ка­кие внеш­ние дан­ные (дан­ные, на ко­то­рые ва­шей функ­ции да­ны ука­за­те­ли или ссыл­ки) вы ак­тив­но ис­поль­зу­е­те внут­ри функ­ции. Сде­лай­те ко­пии этих дан­ных в ло­каль­ных пе­ре­мен­ных, ес­ли вы уве­ре­ны, что эти дан­ные не долж­ны ме­нять­ся по хо­ду ра­бо­ты функ­ции.

  • Ес­ли у вас есть ука­за­те­ли, ко­то­рые ука­зы­ва­ют на дан­ные, не пе­ре­се­каю­щие­ся ни с чем дру­гим в ва­шей функ­ции, то по­мо­ги­те ком­пи­ля­то­ру — ис­поль­зуй­те клю­че­вое сло­во __restrict (не осве­ще­но в дан­ной ста­тье). Осо­бен­но это по­лез­но для ар­гу­мен­тов функ­ций. К со­жа­ле­нию, __restrict не ра­бо­та­ет для ссылок.

  • В ка­че­стве счёт­чи­ка и в усло­вии оста­нов­ки цик­ла ис­поль­зуй­те толь­ко ло­каль­ные пе­ре­мен­ные. Осо­бен­но хо­ро­шо, ко­гда в усло­вии оста­нов­ки цик­ла ис­поль­зу­ет­ся срав­не­ние счёт­чи­ка с ну­лём.

  • В прин­ци­пе, ком­пи­ля­тор мо­жет оп­ти­ми­зи­ро­вать Mat::at до пре­дель­но оп­ти­маль­но­го со­стоя­ния (смот­ри­те ли­стин­ги 4 и 5), но для это­го долж­ны вы­пол­нить­ся не­сколь­ко усло­вий: объ­ект cv::Mat (и, зна­чит, па­ра­мет­ры, вхо­дя­щие в функ­цию at) долж­ны быть ло­каль­ны от­но­си­тель­но функ­ции, и изоб­ра­же­ние не долж­но мо­дифи­ци­ро­вать­ся. Но луч­ше, всё же, ис­поль­зо­вать функ­цию Mat::ptr, так как она ра­бо­та­ет за пре­де­ла­ми внут­рен­не­го цик­ла и, зна­чит, не яв­ля­ет­ся ис­точ­ни­ком не­эф­фек­тив­но­сти.

  • Не ис­поль­зуй­те ите­ра­то­ры для об­хо­да cv::Mat.

18 отзывов на запись «OpenCV: цикл по всем пик­се­лям изоб­ра­же­ния и сов­ме­ще­ние ука­за­те­лей»

It is also possible that Zynga’s chosen advertising network is to blame if we consider the case of the New York Times’ website
generic lisinopril
Cafergot Online
BUY VPXL
Buspar Without A Prescription
Retin-A
antabuse
propranolol online
motilium otc
buy propranolol
cheap anafranil
whoah this blog is fantastic i really like studying your articles. Keep up the good paintings! You understand, many individuals are looking around for this info, you could aid them greatly.
Fluticasone Propionate
sildalis visa
cialis medicine
wellbutrin without prescription
buy nexium
buy fluoxetine

Оставить отзыв

Жёлтые поля обязательны к заполнению

   

Можете использовать теги <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang=""> <div class=""> <span class=""> <br>