Оптимизация сжатия массива
Скажем, у меня есть массив
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 умножает)). Есть ли способ повысить его эффективность?
Ответы
Ответ 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
.