Вариант memcpy, преобразующий и масштабирующий больший тип данных в меньший. Например, от UINT16 до UINT8

У меня есть большой массив данных беззнакового типа 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

Мне нужно преобразовать массив значений, например, {0,..,65535} в линейно преобразованный массив в {0,...,255}, где диапазон {0,...,255} отображается в 0, а все в {65281,..,65535} отображается в 255. Порядок байтов на самом деле не имеет значения, но, конечно, будет иметь значение при использовании черной магии, например reinterpret_cast<..>, как я сделал во втором примере блок кода (который не работает из-за отсутствия реального параметра step).

Markus-Hermann 07.08.2024 08:26

Простого std::transform вам недостаточно?

pptaszni 07.08.2024 08:31

У вас есть насыщенная 255 логика преобразования/фильтрации, которую простое приведение или неявное преобразование не дает. Вам нужно написать для него функцию.

Öö Tiib 07.08.2024 08:40

Для меня это похоже на std::transform + std::back_inserter и позволить компилятору оптимизировать (векторизировать). Быстрый черновик: godbolt.org/z/WK9YP4nWd. В любом случае, если вы считаете, что у вас проблемы с производительностью: измерьте с помощью профилировщика... скорее всего, ваше узкое место где-то еще (пропускная способность памяти, предсказания пропущенных ветвей и т. д.)

Pepijn Kramer 07.08.2024 08:42

@ptaszni: Спасибо. Не знал об этой функции std::transform. Это сводится к петле. Возможно, что-то подобное и будет моим ответом.

Markus-Hermann 07.08.2024 08:55

@PepijnKramer Я верю, что static_cast<std::uint8_t>(value) просто вырезает биты из оригинала и не насыщается на уровне 255.

Öö Tiib 07.08.2024 09:11

Отдельная операция выглядит как std::saturate_cast, однако для ее правильной векторизации вам, вероятно, придется написать собственную оптимизированную реализацию с использованием ассемблера и/или встроенных функций, а не надеяться на компилятор.

user7860670 07.08.2024 09:49

@Markus-Hermann, вопрос не слишком ясен: если вы хотите сопоставить (16-битный или 32-битный) тип данных с (8-битным) типом данных, вы хотите сопоставить его следующим образом: 0xABCD -> 0xAB, 0xCD, или вы заботиться только о последних 8 битах (самый значимый байт) и отбрасывать лишние? если вы хотите сделать это: 0xABCD -> 0xAB, 0xCD, это можно сделать с помощью обычного memcpy. и если вы хотите взять последние 8 бит, выполните приведение, приведение отбросит лишние биты.

Hamza AlAjlouni 07.08.2024 10:00

@ user7860670 Функции C++26 не так широко доступны, и, если они доступны, не следует ожидать, что они будут оптимальными. Есть std::clamp и std::ranges::clamp, которые могут быть лучше.

Öö Tiib 07.08.2024 10:00

@ÖöTiib Ну, это один из тех случаев, когда широко доступная процедура наконец становится стандартом.

user7860670 07.08.2024 10:20

@ÖöTiib приведение было всего лишь кратким примером, фактическая лямбда, передаваемая в преобразование, должна быть изменена, чтобы действительно соответствовать ожидаемому результату.

Pepijn Kramer 07.08.2024 10:35

@Маркус-Германн memcpy также сводится к циклу. Если у вас нет дополнительной информации об этом большом массиве, которая поможет в векторизации, все сведется к циклу.

Useless 07.08.2024 11:25

Поскольку и источник, и пункт назначения не имеют знака, bind_front(ranges::min, numeric_limits<uint8_t>::max()) является хорошим унарным функтором для ranges::transform. Если C++20 недоступен, отбросьте bind_front и поместите std::min в лямбду в качестве унарного функтора для std::transform. В любом случае, прекратите использовать функции памяти, прежде чем наткнетесь на стену UB.

Red.Wave 07.08.2024 21:07
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
13
108
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В вашем случае использование 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, тем самым неявно обеспечивая «шаг», который я просил в своем вопросе. Проверим это предположение и, если оно сработает, примем ваш ответ. Голосование за тесты и хорошее объяснение, которого они уже заслуживают.

Markus-Hermann 08.08.2024 07:35

Другие вопросы по теме