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.

Десять отзывов на запись «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

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

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

   

Можете использовать теги <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>