У меня есть большой массив данных беззнакового типа T*
.
Его следует преобразовать в unsigned char*
, масштабируя значения. Например, при переходе от unsigned short*
к unsigned char*
диапазон {0,..,255} должен быть сопоставлен с 0, а все в {65281,...,65535} — с 255.
Наивно, это можно было бы сделать в цикле, используя что-то вроде битового сдвига:
const T x = some_large_value;
unsigned char y = (x >> (8u * (sizeof(T) - 1u))); //<< Effectively a truncated "x/(256^{sizeof(t)-1})".
Но мне интересно, есть ли способ избежать for (..)
, связанного с таким решением. Мечтаю о super-memcpy
, поддерживающем параметр step
, который можно было бы использовать следующим образом:
// Intel processor. So I guess, little endian is needed. Assume there are N items.
const T* src = ..;
const unsigned char* src_uchar = reinterpret_cast<const unsigned char*>(src);
memcpy(dst_uchar, src_uchar + (sizeof(T) - 1u), N, step=sizeof(T)-1u);
Этот предполагаемый step
-параметр будет пропускать такое количество байтов после каждого скопированного байта. К сожалению, его, конечно, не существует. Но есть ли какая-то другая команда, которая допускает это или его эффект?
Приложение: Добавление рабочей демонстрационной программы после изучения решения
Пташни излучает свет благодарности. Однако, поскольку мне все же потребовалась некоторая обработка, чтобы понять: вот короткая демонстрационная программа, использующая его/ее предложение. Он преобразует массив uint16
в uint8
, уменьшая диапазон значений за счет небольшого сдвига.
#include <algorithm>
#include <iostream>
using namespace std;
int main()
{
// Input A and Output B.
const unsigned short A[] = {60000u, 50000u, 30000u, 1000u, 500u, 300u, 200u};
unsigned char B[] = { 0u, 0u, 0u, 0u, 0u, 0u, 0u};
// lambda expression.
auto transformation = [](const auto& elem)
{ return static_cast<unsigned char>(elem >> 8u * (sizeof(decltype(elem))-1)); };
// Actual downsizing of UINT16 to UINT8.
std::transform(A, A+7, B, transformation);
// Show the results.
for (size_t j=0; j<7; ++j)
{ cout << A[j] << " => " << ((unsigned short)B[j]) << endl; }
return 0;
}
Выход:
60000 => 234
50000 => 195
30000 => 117
1000 => 3
500 => 1
300 => 1
200 => 0
Простого std::transform
вам недостаточно?
У вас есть насыщенная 255 логика преобразования/фильтрации, которую простое приведение или неявное преобразование не дает. Вам нужно написать для него функцию.
Для меня это похоже на std::transform + std::back_inserter и позволить компилятору оптимизировать (векторизировать). Быстрый черновик: godbolt.org/z/WK9YP4nWd. В любом случае, если вы считаете, что у вас проблемы с производительностью: измерьте с помощью профилировщика... скорее всего, ваше узкое место где-то еще (пропускная способность памяти, предсказания пропущенных ветвей и т. д.)
@ptaszni: Спасибо. Не знал об этой функции std::transform
. Это сводится к петле. Возможно, что-то подобное и будет моим ответом.
@PepijnKramer Я верю, что static_cast<std::uint8_t>(value) просто вырезает биты из оригинала и не насыщается на уровне 255.
Отдельная операция выглядит как std::saturate_cast
, однако для ее правильной векторизации вам, вероятно, придется написать собственную оптимизированную реализацию с использованием ассемблера и/или встроенных функций, а не надеяться на компилятор.
@Markus-Hermann, вопрос не слишком ясен: если вы хотите сопоставить (16-битный или 32-битный) тип данных с (8-битным) типом данных, вы хотите сопоставить его следующим образом: 0xABCD -> 0xAB, 0xCD
, или вы заботиться только о последних 8 битах (самый значимый байт) и отбрасывать лишние? если вы хотите сделать это: 0xABCD -> 0xAB, 0xCD
, это можно сделать с помощью обычного memcpy
. и если вы хотите взять последние 8 бит, выполните приведение, приведение отбросит лишние биты.
@ user7860670 Функции C++26 не так широко доступны, и, если они доступны, не следует ожидать, что они будут оптимальными. Есть std::clamp и std::ranges::clamp, которые могут быть лучше.
@ÖöTiib Ну, это один из тех случаев, когда широко доступная процедура наконец становится стандартом.
@ÖöTiib приведение было всего лишь кратким примером, фактическая лямбда, передаваемая в преобразование, должна быть изменена, чтобы действительно соответствовать ожидаемому результату.
@Маркус-Германн memcpy
также сводится к циклу. Если у вас нет дополнительной информации об этом большом массиве, которая поможет в векторизации, все сведется к циклу.
Поскольку и источник, и пункт назначения не имеют знака, bind_front(ranges::min, numeric_limits<uint8_t>::max())
является хорошим унарным функтором для ranges::transform
. Если C++20 недоступен, отбросьте bind_front
и поместите std::min
в лямбду в качестве унарного функтора для std::transform
. В любом случае, прекратите использовать функции памяти, прежде чем наткнетесь на стену UB.
В вашем случае использование std::transform
может быть достаточно хорошим, даже если не таким быстрым, как какая-то измененная версия std::memcpy
. Предполагая, что ваша функция преобразования T --> std::uint8_t
:
auto transformation = [](const auto &elem) {
return static_cast<std::uint8_t>(elem >>
(8u * (sizeof(decltype(elem)) - 1u)));
};
Ваши буферы:
using MyT = std::uint32_t;
// could be any container, also std::array, or a raw pointer (not with ranges)
std::vector<MyT> source_buff(LEN); // let's assume it's initialized with some data already
std::vector<std::uint8_t> dest_buff(LEN, 0);
Вы можете просто написать:
std::transform(source_buff.begin(), source_buff.end(), dest_buff.begin(),
transformation);
// version with execution policy
std::transform(std::execution::par_unseq, source_buff.begin(),
source_buff.end(), dest_buff.begin(), transformation);
Или используя диапазоны:
auto transformedView = source_buff | std::views::transform(transformation);
// note that at this point no transformation took place yet, so you could defer iterating over the new range, or take only the results that you care about, e.g. transformedView[123]
std::ranges::copy(transformedView, std::ranges::begin(dest_buff));
Когда я сравнил эти три подхода (Ubuntu22 с gcc11 -O3) и сравнил их с классическим циклом for
, я получил:
---------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------
JustLoop 544408 ns 544363 ns 1425
StdTransform 133976 ns 133902 ns 4935
StdTransformParUnseq 143085 ns 143082 ns 4823
RangesTransform 134860 ns 134850 ns 5123
это означает, что классический цикл является самым медленным, а использование алгоритмов стандартной библиотеки позволило компилятору использовать некоторые причудливые оптимизации. Аналогичные результаты при использовании QuickBench с Clang 17:
Спасибо! Попытка перефразировать: это лямбда-преобразование действует на UINT16, как и итератор, с которым он связан. Однако выходной итератор действует на UINT8, тем самым неявно обеспечивая «шаг», который я просил в своем вопросе. Проверим это предположение и, если оно сработает, примем ваш ответ. Голосование за тесты и хорошее объяснение, которого они уже заслуживают.
Мне нужно преобразовать массив значений, например, {0,..,65535} в линейно преобразованный массив в {0,...,255}, где диапазон {0,...,255} отображается в 0, а все в {65281,..,65535} отображается в 255. Порядок байтов на самом деле не имеет значения, но, конечно, будет иметь значение при использовании черной магии, например
reinterpret_cast<..>
, как я сделал во втором примере блок кода (который не работает из-за отсутствия реального параметраstep
).