Подтвердить что ты не робот

Оптимизация сжатия массива

Скажем, у меня есть массив k = [1 2 0 0 5 4 0]

Я могу вычислить маску следующим образом m = k > 0 = [1 1 0 0 1 1 0]

Использование только маски m и следующих операций

  • Сдвиг влево/вправо
  • И/или
  • Добавление/Вычитание/Умножение

Я могу записать k в следующее [1 2 5 4]

Вот как я это делаю (псевдокод MATLAB):

function out = compact( in )
    d = in
    for i = 1:size(in, 2) %do (# of items in in) passes
        m = d > 0
        %shift left, pad w/ 0 on right
        ml = [m(2:end) 0] % shift
        dl = [d(2:end) 0] % shift

        %if the data originally has a gap, fill it in w/ the 
        %left shifted one
        use = (m == 0) & (ml == 1) %2 comparison  

        d = use .* dl + ~use .* d

        %zero out elements that have been moved to the left
        use_r = [0 use(1:end-1)]
        d = d .* ~use_r
    end

    out = d(1 : size(find(in > 0), 2)) %truncate the end
end

Интуиция

На каждой итерации мы сдвигаем маску влево и сравниваем маску. Мы устанавливаем индекс, чтобы иметь сдвинутые слева данные, если мы обнаружим, что после этого сдвига теперь был введен индекс, который был первоначально недействительным (mask [i] = 0) (mask [i] = 1).

Вопрос

Вышеупомянутый алгоритм имеет O (N * (3 сдвига + 2 сравнения + AND + add + 3 умножает)). Есть ли способ повысить его эффективность?

4b9b3361

Ответ 1

В исходном псевдокоде нет большой возможности оптимизировать. Здесь я вижу несколько небольших улучшений:

  • может выполнять одну итерацию меньше (то есть размер-1),
  • Если "use" равен нулю, вы можете разорвать цикл раньше,
  • use = (m == 0) & (ml == 1) возможно, может быть упрощен до use = ~m & ml,
  • Если ~ считается отдельной операцией, было бы лучше использовать инвертированную форму: use = m | ~ml, d = ~use .* dl + use .* d, use_r = [1 use(1:end-1)], d = d .*use_r

Но можно изобрести лучшие алгоритмы. И выбор алгоритма зависит от используемых ресурсов процессора:

  • Загрузочный блок, т.е. применяет алгоритм непосредственно к словам памяти. Ничто не может быть сделано здесь, пока чипмейкеры не добавят к параллельным инструкциям SCATTER их инструкции.
  • Регистры SSE, то есть алгоритмы, работающие на все 16 байтов регистров. Алгоритмы, такие как предложенный псевдокод, здесь не могут помочь, потому что у нас уже есть различные команды перетасовки/перестановки, которые улучшают работу. Используя различные команды сравнения с PMOVMSKB, группировка результата на 4 бита и применение различных команд перетасовки в режиме switch/case (как описано LastCoder) - это лучшее, что мы можем сделать.
  • Регистры SSE/AVX с последними наборами команд позволяют лучше подойти. Мы можем непосредственно использовать результат PMOVMSKB, преобразовывая его в регистр управления для чего-то вроде PSHUFB.
  • Целочисленные регистры, т.е. GPR регистрирует или работает одновременно на нескольких частях регистров SSE/AVX DWORD/QWORD (что позволяет выполнять несколько независимых транзакций). Предлагаемый псевдокод, применяемый к целочисленным регистрам, позволяет сжимать двоичные подмножества любой длины (от 2 до 20 бит). Вот мой алгоритм, который, скорее всего, будет работать лучше.

С++, 64 бит, ширина подмножества = 8:

typedef unsigned long long ull;
const ull h = 0x8080808080808080;
const ull l = 0x0101010101010101;
const ull end = 0xffffffffffffffff;

// uncompacted bytes
ull x = 0x0100802300887700;

// set hi bit for zero bytes (see D.Knuth, volume 4)
ull m = h & ~(x | ((x|h) - l));

// bitmask for nonzero bytes
m = ~(m | (m - (m>>7)));

// tail zero bytes need no special treatment
m |= (m - 1);

while (m != end)
{
  ull tailm = m ^ (m + 1); // bytes to be processed
  ull tailx = x & tailm; // get the bytes
  tailm |= (tailm << 8); // shift 1 byte at a time
  m |= tailm; // all processed bytes are masked
  x = (x ^ tailx) | (tailx << 8); // actual byte shift
}

Ответ 2

Итак, вам нужно выяснить, стоит ли лишняя parallelism, смещение/перемещение накладных расходов для такой простой задачи.

for(int inIdx = 0, outIdx = 0; inIdx < inLength; inIdx++) {
 if(mask[inIdx] == 1) {
  out[outIdx] = in[inIdx];
  outIdx++;
 }
}

Если вы хотите перейти на параллельный маршрут SIMD, лучшим вариантом будет SWITCH CASE со всеми возможными перестановками следующих 4 бит маски. Почему бы не 8? потому что команда PSHUFD может перемещаться только на XMMX m128, а не на YMMX m256.

Итак, вы делаете 16 случаев:

  • [1 1 1 1], [1 1 1 0], [1 1 0 0], [1 0 0 0], [0 0 0 0] не требуется никакого специального сдвига/перетасовки, вы просто скопируете ввод на выходной MOVDQU и увеличение указателя вывода на 4, 3, 2, 1, 0 соответственно.
  • [0 1 1 1], [0 0 1 1], [0 1 1 0], [0 0 0 1], [0 1 0 0], [0 0 1 0] вам просто нужно использовать PSRLx (shift right logical) и увеличивать указатель на 3, 2, 2, 1, 1, 1 соответственно
  • [1 0 0 1], [1 0 1 0], [0 1 0 1], [1 0 1 1], [1 1 0 1] вы используете PSHUFD для упаковки вашего ввода, а затем увеличиваете свой указатель вывода на 2, 2, 2, 3, 3 соответственно.

Таким образом, каждый случай будет минимальным количеством обработки (от 1 до 2 инструкций SIMD и добавления выходного указателя). Окружающий цикл операторов case обрабатывал бы добавление указателя константы (на 4) и MOVDQA для загрузки ввода.

Ответ 3

Исходный код перемещает элемент массива только по одному шагу за раз. Это может быть улучшено. Можно группировать элементы массива и сразу менять их на 2 ^ k шагов.

Первая часть этого алгоритма вычисляет, сколько шагов нужно сдвигать каждый элемент. Вторая часть перемещает элементы - сначала на один шаг, затем на 2, затем на 4 и т.д. Это работает правильно, а элементы не смешиваются, потому что после каждой смены достаточно места для выполнения сдвига в 2 раза больше.

Matlab, код не проверен:

function out = compact( in )
    m = in <= 0
    for i = 1:size(in, 2)-1
        m = [0 m(1:end-1)]
        s = s + m
    end

    d = in
    shift = 1
    for j = 1:ceil(log2(size(in, 2)))
        s1 = rem(s, 2)
        s = (s - s1) / 2
        d = (d .* ~s1) + ([d(1+shift:end) zeros(1,shift)] .* [s1(1+shift:end) zeros(1,shift)])
        shift = shift*2
    end
    out = d
end

Вышеупомянутая сложность алгоритма - O (N * (1 shift + 1 add) + log (N) * (1 rem + 2 add + 3 mul + 2 shift)).

Ответ 4

Считая комментарии ниже исходного вопроса, в реальной задаче массив содержит 32-битные числа с плавающей запятой, а маска (одно?) 32-битное целое, поэтому я не понимаю, почему смены и т.д. должны для уплотнения массива. Простой алгоритм сжатия (в C) будет примерно таким:

float array[8];
unsigned int mask = ...;
int a = 0, b = 0;
while (mask) {
  if (mask & 1) { array[a++] = array[b]; }
  b++;
  mask >>= 1;
}
/* Size of compacted array is 'a' */
/* Optionally clear the rest: */
while (a < 8) array[a++] = 0.0;

Незначительные изменения будут связаны с порядком бит в маске, но необходимы только операции ALU, которые являются обновлениями индексации переменных и смещением и Инициализацией маски. Поскольку исходный массив имеет ширину не менее 256 бит, обычный процессор не может смещать весь массив по-разному.

Ответ 5

Предполагая, что вы хотите хранить только положительные целые числа из массива с минимальными шагами в С++, это пример кода:

int j = 0;
int arraysize = (sizeof k)/4;
int store[arraysize];
for(int i = 0; i<arraysize; i++)
{
    if(k[i] > 0)
    {
        store[j] = k[i];
        j++;
    }
}

Или вы можете напрямую использовать элементы k [], если вы не хотите использовать цикл for.