Производительность C ++: проверка блока памяти на наличие определенных значений в определенных ячейках - PullRequest
3 голосов
/ 11 февраля 2011

Я занимаюсь исследованием алгоритмов 2D Bin Packing. Я задал аналогичный вопрос относительно производительности PHP - он был слишком медленным для упаковки - и теперь код конвертируется в C ++.

Это все еще довольно медленно. Поэтому моя программа выделяет блоки динамической памяти и заполняет их символом 'o'

char* bin;
bin = new (nothrow) char[area];
if (bin == 0) {
    cout << "Error: " << area << " bytes could not be allocated";
    return false;
}
for (int i=0; i<area; i++) {
    bin[i]='o';
}

(их размер от 1 до 30 КБ для моих наборов данных)

Затем программа проверяет различные комбинации символов «x» внутри текущего блока памяти.

void place(char* bin, int* best, int width)
{   
    for (int i=best[0]; i<best[0]+best[1]; i++)
        for (int j=best[2]; j<best[2]+best[3]; j++)
            bin[i*width+j] = 'x';
}

Одна из функций, которая проверяет неперекрывающиеся значения, вызывается миллионы раз во время выполнения.

bool fits(char* bin, int* pos, int width)
{   
    for (int i=pos[0]; i<pos[0]+pos[1]; i++)
        for (int j=pos[2]; j<pos[2]+pos[3]; j++)
            if (bin[i*width+j] == 'x')
                return false;
    return true;
}

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

Поскольку у меня есть только две опции 'x' и 'o', я мог бы попытаться использовать только один бит вместо целого байта, который принимает символ. Но меня больше волнует скорость, ты думаешь, это ускорит процесс?

Спасибо!

Обновление: я заменил int* pos на rect pos (то же самое для best), как предложил MSalters. Сначала я увидел улучшение, но я тестировал больше с большими наборами данных, и, похоже, он вернулся к нормальному времени выполнения. Я попробую другие предложенные методы и буду держать вас в курсе.

Обновление: использование memset и memchr ускорило процесс примерно в два раза. Замена 'x' и 'o' на '\ 1' и '\ 0' не показала никаких улучшений. __restrict тоже не помог. В целом, я доволен производительностью программы сейчас, так как я также внес некоторые улучшения в сам алгоритм. Я еще не попробовал использовать растровое изображение и компилировать с -02 (-03) ... Еще раз спасибо всем.

Ответы [ 8 ]

2 голосов
/ 11 февраля 2011

[Конечно: профиль!]

Использование бита вместо байта не будет быстрее в первом случае.

Однако учтите, что с символами вы можете привести блоки по 4 или 8 байтов к 32-битным или 64-битным целым без знака (убедитесь, что вы обрабатываете выравнивание), и сравнить это со значением для «oooo» или «oooooooo» в блок. Это позволяет очень быстро сравнивать.

Теперь, перейдя к целочисленному подходу, вы можете увидеть, что вы можете сделать то же самое с битовым подходом и обрабатывать, скажем, 64 бита в одном сравнении. Это, безусловно, должно дать реальную скорость.

2 голосов
/ 11 февраля 2011

Лучшая возможность - использовать алгоритм с большей сложностью.

Но даже ваш текущий алгоритм может быть ускорен.Попробуйте использовать инструкции SSE для тестирования ~ 16 байт одновременно, также вы можете сделать одно большое выделение и разделить его самостоятельно, это будет быстрее, чем использование распределителя библиотек (распределитель библиотек имеет то преимущество, что позволяет вам освобождать блоки по отдельности, но яне думаю, что вам нужна эта функция).

1 голос
/ 11 февраля 2011

Самое большое улучшение, которое я ожидаю, это нетривиальное изменение:

// changed pos to class rect for cleaner syntax
bool fits(char* bin, rect pos, int width)
{
    if (bin[pos.top()*width+pos.left()] == 'x')
                return false;
    if (bin[(pos.bottom()-1*width+pos.right()] == 'x')
                return false;
    if (bin[(pos.bottom()*width+pos.left()] == 'x')
                return false;
    if (bin[pos.top()*width+pos.right()] == 'x')
                return false;

    for (int i=pos.top(); i<=pos.bottom(); i++)
        for (int j=pos.left(); j<=pos.right(); j++)
            if (bin[i*width+j] == 'x')
                return false;
    return true;
}

Конечно, вы тестируете bin[(pos.bottom()-1*width+pos.right()] дважды. Но в первый раз вы делаете это намного раньше в алгоритме. Вы добавляете блоки, что означает, что существует сильная корреляция между смежными корзинами. Поэтому, сначала проверяя углы, вы часто возвращаетесь намного раньше. Вы могли бы даже рассмотреть добавление 5-го чека в середине.

1 голос
/ 11 февраля 2011

Прежде всего, вы не забыли сказать своему компилятору оптимизировать?

И отключить медленную проверку границ индекса массива и тому подобное?

После этого вы получите существенное ускорениепредставляя ваши двоичные значения в виде отдельных битов, поскольку вы можете устанавливать или сбрасывать, скажем, 32 или 64 бита за раз.

Также я бы предположил, что динамическое распределение даст значительную долю служебных данных, нопо-видимому, вы измерили и обнаружили, что это не так.Однако, если управление памятью действительно вносит значительный вклад во время, решение зависит в некоторой степени от схемы использования.Но, возможно, ваш код генерирует стековое поведение alloc / free, и в этом случае вы можете оптимизировать распределение практически до нуля;просто выделите большой кусок памяти в начале, а затем выделите из него подобный стеку

Учитывая ваш текущий код:

void place(char* bin, int* best, int width)
{   
    for (int i=best[0]; i<best[0]+best[1]; i++)
        for (int j=best[2]; j<best[2]+best[3]; j++)
            bin[i*width+j] = 'x';
}

Из-за возможного наложения псевдонимов компилятор может не реализоватьнапример, best[0] будет постоянным во время цикла.

Итак, скажите ему:

void place(char* bin, int const* best, int const width)
{
    int const maxY = best[0] + best[1];
    int const maxX = best[2] + best[3];

    for( int y = best[0]; y < maxY; ++y )
    {
        for( int x = best[2]; x < maxX; ++x )
        {
            bin[y*width + x] = 'x';
        }
    }
}

Скорее всего, ваш компилятор выведет вычисление y*width из внутреннего цикла, нопочему бы не сказать ему, сделайте также, что:

void place(char* bin, int* best, int const width)
{
    int const maxY = best[0]+best[1];
    int const maxX = best[2]+best[3];

    for( int y = best[0]; y < maxY; ++y )
    {
        int const startOfRow  = y*width;

        for( int x = best[2]; x < maxX; ++x )
        {
            bin[startOfRow + x] = 'x';
        }
    }
}

Эта ручная оптимизация (также применяемая к другой процедуре) может или не может помочь, это зависит от того, насколько умен ваш компилятор.

Далее,если это не поможет, рассмотрите возможность замены внутреннего цикла на std::fill (или memset), сделав целый ряд одним махом.

И если это не помогает или не помогаетдостаточно переключиться на представление на битовом уровне.

Возможно, стоит отметить и попробовать, что каждый ПК имеет встроенную аппаратную поддержку для оптимизации операций на битовом уровне, а именно карту графического ускорителя (в старыхвремена называются бличип ттер).Таким образом, вы можете просто использовать библиотеку изображений и черно-белое растровое изображение.Но так как ваши прямоугольники маленькие, я не уверен, что издержки установки перевесят скорость фактической операции - нужно измерить.; -)

Приветствия & hth.,

1 голос
/ 11 февраля 2011

Растровые изображения также повысят скорость, поскольку они затрагивают меньше памяти и, таким образом, приводят к увеличению количества обращений к памяти из кэша.Также в place вы можете скопировать элементы best в локальные переменные, чтобы компилятор знал, что ваши записи в bin не изменят best.Если ваш компилятор поддерживает некоторое написание restrict, вы можете использовать это.Вы также можете заменить внутренний цикл в place библиотечной функцией memset, а внутренний цикл в fits на memchr;однако, это не может быть значительным улучшением производительности.

0 голосов
/ 12 февраля 2011

Я бы подумал о разрыве кеша памяти. Эти функции проходят через подматрицы внутри большей матрицы - я полагаю, во много раз больше по ширине и высоте. Это означает, что маленькие строки матрицы являются непрерывной памятью, но между строками это может нарушить страницы кэша памяти. Подумайте о представлении больших ячеек матрицы в памяти в таком порядке, чтобы элементы подматрицы находились как можно ближе друг к другу. Это вместо того, чтобы хранить вектор смежных полных строк. Первый вариант мне приходит в голову, это рекурсивно разбивать вашу большую матрицу на матрицы размером [2 ^ i, 2 ^ i] упорядоченные {верхний левый, верхний правый, нижний левый, нижний правый}.

1) т.е. если ваша матрица имеет размер [X, Y], представленный в массиве размера X * Y, тогда элемент [x, y] находится в позиции (x, y) в массиве:

используйте вместо (y * X + x):

unsigned position( rx, ry )
{
  unsigned x = rx;
  unsigned y = rx;
  unsigned part = 1;
  unsigned pos = 0;
  while( ( x != 0 ) && ( y != 0 ) ) {
    unsigned const lowest_bit_x = ( x % 2 );
    unsigned const lowest_bit_y = ( y % 2 );
    pos += ( ((2*lowest_bit_y) + lowest_bit_x) * part );
    x /= 2; //throw away lowest bit
    y /= 2;
    part *= 4; //size grows by sqare(2)
  }
  return pos;
}

Я не проверял этот код, просто чтобы объяснить, что я имею в виду. Если вам нужно, попробуйте найти более быстрый способ реализации.

но обратите внимание, что выделенный вами массив будет больше, чем X * Y, он должен быть как можно меньшим (2 ^ (2 * k)), и это будет бесполезно, если X и Y не имеют одинаковый размерный масштаб. Но это можно решить, разбив сначала большую матрицу на квадраты.

И тогда преимущества кэша могут превзойти более сложную позицию (x, y).

2) затем попытайтесь найти лучший способ пробежаться по элементам подматрицы в функциях fits () и place (). Еще не уверен, что это такое, не обязательно, как вы делаете сейчас. По существу, подматрица размера [x, y] должна разбиваться не более чем на блоки y * log (x) * log (y), которые являются смежными в представлении массива, но все они помещаются не более чем в 4 блока размера 4 * х * у. Итак, наконец, для матриц, которые меньше, чем страница кэша памяти, вы получите не более 4 разрывов кэша памяти, в то время как ваш исходный код может сломаться y раз.

0 голосов
/ 12 февраля 2011

Если у вас есть 2 значения для вашего основного типа, я сначала попытался бы использовать bool.Тогда компилятор знает, что у вас есть 2 значения и может быть в состоянии оптимизировать некоторые вещи лучше.Приступайте к этому добавлению const, где это возможно (например, параметр fits (bool const *, ...)).

0 голосов
/ 11 февраля 2011

Помимо обязательного заявления об использовании профилировщика, приведенный выше совет о замене объектов битовой картой - очень хорошая идея.Если вам это не нравится ..

Попробуйте заменить

for (int i=0; i<area; i++) {
    bin[i]='o';
}

на

memset(bin, 'o', area);

Обычно memset будет быстрее, так какон компилируется в меньшее количество машинного кода.

Также

void place(char* bin, int* best, int width)
{   
    for (int i=best[0]; i<best[0]+best[1]; i++)
        for (int j=best[2]; j<best[2]+best[3]; j++)
            bin[i*width+j] = 'x';
}

имеет немного места для улучшения

void place(char* bin, int* best, int width)
{   
    for (int i=best[0]; i<best[0]+best[1]; i++)

        memset(                         (i * width)  + best[2], 
                'x', 
                (best[2] + best[3]) - (((i * width)) + best[2]) + 1); 
}

путем устранения одного из циклов.

Последняя идея - изменить представление данных.Подумайте об использовании символа '\ 0' в качестве замены вашего 'o' и '\ 1' в качестве замены вашего символа 'x'.Это похоже на использование битовой карты.

Это позволит вам тестировать вот так.

if (best[1])
{
    // Is a 'x'
}
else
{
    // Is a 'o'
}

, что может привести к более быстрому коду.Опять же, профилировщик - ваш друг:)

Это представление также позволит вам просто суммировать набор символов, чтобы определить, сколько «х» и «о».

int sum = 0;
for (int i = 0; i < 12; i++)
{
    sum += best[i];
}

cout << "There are " << sum << "'x's in the range" << endl;

Лучшийудачи тебе

зло.

...