Превратите большой кусок памяти назад, быстро - PullRequest
13 голосов
/ 28 января 2010

Мне нужно переписать около 4 КБ данных в обратном порядке, на уровне битов (последний бит последнего байта становится первым битом первого байта), как можно быстрее. Есть какие-нибудь умные отрывки, чтобы сделать это?

Обоснование: данные отображают содержимое ЖК-экрана во встроенном устройстве, которое обычно располагается таким образом, чтобы экран находился на уровне ваших плеч. Экран имеет ориентацию «6 часов», то есть следует смотреть снизу - как лежащий на полу или висящий над уровнем ваших глаз. Это можно исправить, повернув экран на 180 градусов, но затем мне нужно обратить данные экрана (сгенерированные библиотекой), которые составляют 1 бит = 1 пиксель, начиная с верхнего левого края экрана. Процессор не очень мощный, и у устройства уже достаточно работы, плюс было бы желательно несколько кадров в секунду, поэтому производительность является проблемой; Оперативной памяти не так много.

редактировать: Одноядерный, ARM 9 серии. 64 МБ, (будет уменьшено до 32 МБ позже), Linux. Данные передаются из системной памяти в драйвер ЖК-дисплея через 8-битный порт ввода-вывода.

Процессор 32-битный и работает намного лучше при этом размере слова, чем на уровне байтов.

Ответы [ 9 ]

22 голосов
/ 28 января 2010

Есть классический способ сделать это. Допустим, unsigned int - это ваше 32-битное слово. Я использую C99, потому что ключевое слово restrict позволяет компилятору выполнять дополнительные оптимизации в этом критичном по скорости коде, который в противном случае был бы недоступен. Эти ключевые слова сообщают компилятору, что "src" и "dest" не перекрываются. Это также предполагает, что вы копируете целое число слов, если нет, то это только начало.

Я также не знаю, какие примитивы сдвига / вращения битов бывают быстрыми на ARM, а какие медленными. Это то, что нужно учитывать. Если вам нужна большая скорость, рассмотрите возможность дизассемблирования выходных данных компилятора C и перехода оттуда. Если вы используете GCC, попробуйте O2, O3 и Os, чтобы увидеть, какой из них самый быстрый. Вы можете уменьшить количество остановок в конвейере, выполнив два слова одновременно.

Используется 23 операции на слово, не считая загрузки и сохранения. Однако эти 23 операции выполняются очень быстро, и ни одна из них не обращается к памяти. Я не знаю, будет ли справочная таблица быстрее или нет.

void
copy_rev(unsigned int *restrict dest,
         unsigned int const *restrict src,
         unsigned int n)
{
    unsigned int i, x;
    for (i = 0; i < n; ++i) {
        x = src[i];
        x = (x >> 16) | (x << 16);
        x = ((x >> 8) & 0x00ff00ffU) | ((x & 0x00ff00ffU) << 8);
        x = ((x >> 4) & 0x0f0f0f0fU) | ((x & 0x0f0f0f0fU) << 4);
        x = ((x >> 2) & 0x33333333U) | ((x & 0x33333333U) << 2);
        x = ((x >> 1) & 0x55555555U) | ((x & 0x555555555) << 1);
        dest[n-1-i] = x;
    }
}

Эта страница является отличным справочником: http://graphics.stanford.edu/~seander/bithacks.html#BitReverseObvious

Последнее замечание. Если посмотреть на ссылку на сборку ARM, есть код операции «REV», который меняет порядок байтов в слове. Это сократит 7 операций за цикл из вышеприведенного кода.

16 голосов
/ 28 января 2010

Самым быстрым способом, вероятно, будет сохранение обратной последовательности всех возможных значений байтов в справочной таблице. Таблица будет занимать всего 256 байт.

14 голосов
/ 28 января 2010

Создание 256-элементной таблицы поиска значений байтов, которые инвертированы по битам из их индекса.

{0x00, 0x80, 0x40, 0xc0 и т. Д.}

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

Если вы пишете на ассемблере, в наборе команд x86 есть инструкция XLAT, которая выполняет только этот поиск. Хотя на современных процессорах это может быть не так быстро, как на С-коде.

Вы можете сделать это на месте, если итерируете от обоих концов к середине. Из-за эффектов кеширования вы можете обнаружить, что быстрее поменять местами 16-байтовые фрагменты (при условии, что 16-байтовая строка кэша).

Вот основной код (не включая оптимизацию строки кэша)

// bit reversing lookup table
typedef unsigned char BYTE;
extern const BYTE g_RevBits[256];

void ReverseBitsInPlace(BYTE * pb, int cb)
{
    int iter = cb/2;
    for (int ii = 0, jj = cb-1; ii < iter; ++ii, --jj)
    {
        BYTE b1 = g_RevBits[pb[ii]];
        pb[ii] = g_RevBits[pb[jj]];
        pb[jj] = b1;
    }

    if (cb & 1) // if the number of bytes was odd, swap the middle one in place
    {
       pb[cb/2] = g_RevBits[pb[cb/2]];
    }
}

// initialize the bit reversing lookup table using macros to make it less typing.
#define BITLINE(n) \
   0x0##n, 0x8##n, 0x4##n, 0xC##n, 0x2##n, 0xA##n, 0x6##n, 0xE##n,\
   0x1##n, 0x9##n, 0x5##n, 0xD##n, 0x3##n, 0xB##n, 0x7##n, 0xF##n,

const BYTE g_RevBits[256] = {
  BITLINE(0), BITLINE(8), BITLINE(4), BITLINE(C), 
  BITLINE(2), BITLINE(A), BITLINE(6), BITLINE(E), 
  BITLINE(1), BITLINE(9), BITLINE(5), BITLINE(D), 
  BITLINE(3), BITLINE(B), BITLINE(7), BITLINE(F), 
  };
9 голосов
/ 28 января 2010

Сайт Bit Twiddling Hacks является хорошей отправной точкой для решения подобных проблем. Посмотрите здесь для быстрого обращения битов. Затем вы можете применить его к каждому байту / слову вашего блока памяти.

EDIT:

Вдохновленный ответом Дитриха Эппса и глядя на набор команд ARM , существует код операции RBIT, который обращает биты, содержащиеся в регистре. Поэтому, если производительность критична, вы можете рассмотреть возможность использования некоторого кода сборки.

3 голосов
/ 14 мая 2013

enter image description here

Похоже, этот код требует около 50 тактов на бит на моей машине i7 XPS 8500. 7,6 секунды для миллиона переворотов массива. Однопоточный. Он печатает некоторые искусства ASCI, основанные на шаблонах 1 и 0. Я повернул картинку влево на 180 градусов после обращения битового массива, используя графический редактор, и они выглядят одинаково для меня. Двойное изображение получается так же, как оригинал.

Что касается плюсов, то это комплексное решение. Он переставляет биты с задней части массива битов на фронт, вместо того, чтобы работать с битами / байтами, а затем необходимо поменять местами байты / массивы.

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

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

// Reverse BitsInBuff.cpp : Defines the entry point for the console application.
#include "stdafx.h"
#include "time.h"
#include "memory.h"
//
//  Manifest constants
#define  uchar unsigned char
#define  BUFF_BYTES 510 //400 supports a display of 80x40 bits
#define  DW 80  //  Display Width
// ----------------------------------------------------------------------------
uchar   mask_set[] = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80 };
uchar   mask_clr[] = { 0xfe, 0xfd, 0xfb, 0xf7, 0xef, 0xdf, 0xbf, 0x7f };
//
// Function Prototypes
static void PrintIntBits(long x, int bits);
void    BitSet(uchar * BitArray, unsigned long BitNumber);
void    BitClr(uchar * BitArray, unsigned long BitNumber);
void    BitTog(uchar * BitArray, unsigned long BitNumber);
uchar   BitGet(uchar * BitArray, unsigned long BitNumber);
void    BitPut(uchar * BitArray, unsigned long BitNumber, uchar value);
//
uchar *ReverseBitsInArray(uchar *Buff, int BitKnt);
static void PrintIntBits(long x, int bits);
// -----------------------------------------------------------------------------
// Reverse the bit ordering in an array
uchar *ReverseBitsInArray(uchar *Buff, int BitKnt)  {
    unsigned long front=0, back = BitKnt-1;
    uchar temp;
    while( front<back ) {
        temp = BitGet(Buff, front);                 // copy front bit to temp before overwriting 
        BitPut(Buff, front, BitGet(Buff, back));    // copy back bit to front bit
        BitPut(Buff, back, temp);                   // copy saved value of front in temp to back of bit arra)
        front++;
        back--;
    }
    return Buff;
}
//  ---------------------------------------------------------------------------
//  ---------------------------------------------------------------------------
int _tmain(int argc, _TCHAR* argv[])    {
    int i, j, k, LoopKnt = 1000001;
    time_t start;
    uchar Buff[BUFF_BYTES];
    memset(Buff, 0, sizeof(Buff));
    //  make an ASCII art picture
    for(i=0, k=0; i<(sizeof(Buff)*8)/DW; i++)   {
        for(j=0; j<DW/2; j++)   {
            BitSet(Buff, (i*DW)+j+k);
        }
        k++;
    }
    //  print ASCII art picture
    for(i=0; i<sizeof(Buff); i++)   {
        if(!(i % 10)) printf("\n"); // print bits in blocks of 80
        PrintIntBits(Buff[i], 8);
    }
    i=LoopKnt;
    start = clock();
    while( i-- )    {
        ReverseBitsInArray((uchar *)Buff, BUFF_BYTES * 8);
    }
    //  print ASCII art pic flipped upside-down and rotated left
    printf("\nMilliseconds elapsed = %d", clock() - start);
    for(i=0; i<sizeof(Buff); i++)   {
        if(!(i % 10)) printf("\n"); // print bits in blocks of 80
        PrintIntBits(Buff[i], 8);
    }
    printf("\n\nBenchmark time for %d loops\n", LoopKnt);
    getchar();
    return 0;
}
// -----------------------------------------------------------------------------
//  Scaffolding...
static void PrintIntBits(long x, int bits)  {
    unsigned long long z=1;
    int i=0;
    z = z << (bits-1);
    for (; z > 0; z >>= 1)  {
        printf("%s", ((x & z) == z) ? "#" : ".");
    }
}
//  These routines do bit manipulations on a bit array of unsigned chars
//  ---------------------------------------------------------------------------
void BitSet(uchar *buff, unsigned long BitNumber)   {
    buff[BitNumber >> 3] |= mask_set[BitNumber & 7];
}
//  ---------------------------------------------------------------------------- 
void BitClr(uchar *buff, unsigned long BitNumber)   {
    buff[BitNumber >> 3] &= mask_clr[BitNumber & 7];
}
//  ---------------------------------------------------------------------------- 
void BitTog(uchar *buff, unsigned long BitNumber)   {
    buff[BitNumber >> 3] ^= mask_set[BitNumber & 7];
}
//  ---------------------------------------------------------------------------- 
uchar BitGet(uchar *buff, unsigned long BitNumber)  {
    return (uchar)  ((buff[BitNumber >> 3] >> (BitNumber & 7)) & 1);
}
//  ---------------------------------------------------------------------------- 
void BitPut(uchar *buff, unsigned long BitNumber, uchar value)  {
    if(value)   {   // if the bit at buff[BitNumber] is true.
        BitSet(buff, BitNumber);
    }   else    {
        BitClr(buff, BitNumber);
    }
}

Ниже приведен список кодов для оптимизации с использованием нового буфера вместо замены байтов на месте. Учитывая, что только 2030: 4080 BitSet () необходимы из-за теста if (), и около половины GetBit () и PutBits () исключаются путем исключения TEMP, я подозреваю, что время доступа к памяти является большой фиксированной стоимостью эти виды операций, обеспечивающие жесткий предел оптимизации.

Использование подхода поиска и условной замены байтов, а не битов, уменьшает в 8 раз количество обращений к памяти, а проверка на 0 байтов амортизируется по 8 битам, а не 1.

Используя эти два подхода вместе, тестирование, чтобы увидеть, равен ли 0 весь 8-битный символ, перед выполнением НИЧЕГО, включая поиск в таблице и запись, вероятно, будет самым быстрым из возможных подходов, но потребует дополнительных 512 байты для нового, битового массива назначения и 256 байтов для таблицы поиска. Выигрыш в производительности может быть довольно драматичным, хотя.

// -----------------------------------------------------------------------------
// Reverse the bit ordering in new array
uchar *ReverseBitsInNewArray(uchar *Dst, const uchar *Src, const int BitKnt)    {
    int front=0, back = BitKnt-1;
    memset(Dst, 0, BitKnt/BitsInByte);

    while( front < back )   {
        if(BitGet(Src, back--)) {   // memset() has already set all bits in Dst to 0, 
            BitSet(Dst, front);     // so only reset if Src bit is 1
        }
        front++;
    }
    return Dst;
3 голосов
/ 28 января 2010

Перебрать половину массива, конвертировать и обмениваться байтами.

for( int i = 0; i < arraySize / 2; i++ ) {
    char inverted1 = invert( array[i] );
    char inverted2 = invert( array[arraySize - i - 1] );
    array[i] = inverted2;
    array[arraySize - i - 1] = inverted1;
}

Для преобразования используйте предварительно вычисленную таблицу - массив из 2 CHAR_BIT (CHAR_BIT, скорее всего, будет 8) элементов, где в позиции "I" сохраняется результат байта со значением "I" инверсии , Это будет очень быстро - один проход - и потреблять только 2 CHAR_BIT для таблицы.

2 голосов
/ 28 января 2010

Чтобы обратить один байт x, вы можете обрабатывать биты по одному:

unsigned char a = 0;
for (i = 0; i < 8; ++i) {
   a += (unsigned char)(((x >> i) & 1) << (7 - i));
}

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

Тогда вам нужно просто обратить байтовый массив, и когда вы записываете данные, примените приведенное выше отображение. Обращение массива байтов является хорошо документированной проблемой, например, здесь .

1 голос
/ 14 мая 2013

Данные передаются из системной памяти в драйвер ЖКД через 8-битный ввод-вывод порт.

Поскольку вы будете записывать на ЖК-дисплей по одному байту за раз, я думаю, что лучшей идеей будет выполнять инвертирование битов при отправке данных в драйвер ЖК-дисплея, а не как отдельный -проходить. Что-то в этом духе должно быть быстрее, чем любой другой ответ:

void send_to_LCD(uint8_t* data, int len, bool rotate) {
  if (rotate)
    for (int i=len-1; i>=0; i--)
      write(reverse(data[i]));
  else
    for (int i=0; i<len; i++)
      write(data[i]);
}

Где write() - это функция, которая отправляет байт в драйвер ЖК-дисплея, и reverse() один из методов обращения однобайтовых битов, описанных в других ответах.

Этот подход избавляет от необходимости хранить две копии видеоданных в оперативной памяти, а также избегает двустороннего чтения-инвертирования-записи. Также обратите внимание, что это простейшая реализация: она может быть легко адаптирована для загрузки, скажем, 4 байта за раз из памяти, если это приведет к повышению производительности. Умный векторизованный компилятор может даже сделать это за вас.

1 голос
/ 28 января 2010

Одноядерный?

Сколько памяти?

Буферизуется ли дисплей в памяти и передается ли на устройство, или это единственная копия пикселей в памяти экранов?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...