向大数组减去或添加常量
Subtract or add constant to large array
我有一个很大的 uint8_t
数组(大小 = 1824 * 942)。我想对每个元素做同样的操作。特别是我需要从每个元素中减去 -15。
这个数组每秒刷新 20 次,所以时间是个问题,我正在避免在数组上循环。
有没有简单的方法来做到这一点?
你可以只写一个带有普通循环的函数:
void add(uint8_t* a, size_t a_len, uint8_t b) {
for(uint8_t* ae = a + a_len; a < ae; ++a)
*a += b;
}
并希望编译器为您矢量化它,它确实做到了,请参阅 assembly。
std::for_each
和 std::transform
的解决方案,例如:
void add(uint8_t* a, size_t a_len, uint8_t b) {
std::transform(a, a + a_len, a, [b](auto value) { return value + b; });
}
应该生成完全相同的代码,但有时却不会。
[更新]
出于好奇,我对以下解决方案进行了基准测试:
#include <benchmark/benchmark.h>
#include <cstdint>
#include <array>
#include <algorithm>
#include <immintrin.h>
constexpr size_t SIZE = 1824 * 942;
alignas(32) std::array<uint8_t, SIZE> A;
__attribute__((noinline)) void add_loop(uint8_t* a, size_t a_len, uint8_t b) {
for(uint8_t* ae = a + a_len; a < ae; ++a)
*a += b;
}
__attribute__((noinline)) void add_loop_4way(uint8_t* a, size_t a_len, uint8_t b) {
a_len /= 4;
for(uint8_t* ae = a + a_len; a < ae; ++a) {
a[a_len * 0] += b;
a[a_len * 1] += b;
a[a_len * 2] += b;
a[a_len * 3] += b;
}
}
__attribute__((noinline)) void add_transform(uint8_t* a, size_t a_len, uint8_t b) {
std::transform(a, a + a_len, a, [b](auto value) { return value + b; });
}
inline void add_sse_(__m128i* sse_a, size_t a_len, uint8_t b) {
__m128i sse_b = _mm_set1_epi8(b);
for(__m128i* ae = sse_a + a_len / (sizeof *sse_a / sizeof b); sse_a < ae; ++sse_a)
*sse_a = _mm_add_epi8(*sse_a, sse_b);
}
__attribute__((noinline)) void add_sse(uint8_t* a, size_t a_len, uint8_t b) {
add_sse_(reinterpret_cast<__m128i*>(a), a_len, b);
}
inline void add_avx_(__m256i* avx_a, size_t a_len, uint8_t b) {
__m256i avx_b = _mm256_set1_epi8(b);
for(__m256i* ae = avx_a + a_len / (sizeof *avx_a / sizeof b); avx_a < ae; ++avx_a)
*avx_a = _mm256_add_epi8(*avx_a, avx_b);
}
__attribute__((noinline)) void add_avx(uint8_t* a, size_t a_len, uint8_t b) {
add_avx_(reinterpret_cast<__m256i*>(a), a_len, b);
}
template<decltype(&add_loop) F>
void B(benchmark::State& state) {
for(auto _ : state)
F(A.data(), A.size(), 15);
}
BENCHMARK_TEMPLATE(B, add_loop);
BENCHMARK_TEMPLATE(B, add_loop_4way);
BENCHMARK_TEMPLATE(B, add_transform);
BENCHMARK_TEMPLATE(B, add_sse);
BENCHMARK_TEMPLATE(B, add_avx);
BENCHMARK_MAIN();
i7-7700k CPU 和 g++-8.3 -DNDEBUG -O3 -march=native -mtune=native
上的结果:
------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
B<add_loop> 31589 ns 31589 ns 21981
B<add_loop_4way> 30030 ns 30030 ns 23265
B<add_transform> 31590 ns 31589 ns 22159
B<add_sse> 39993 ns 39992 ns 17403
B<add_avx> 31588 ns 31587 ns 22161
循环、转换和 AVX2 版本的时间几乎相同。
SSE 版本较慢,因为编译器生成更快的 AVX2 代码。
perf report
报告 ~50% L1d-cache 未命中率,这表明该算法因内存访问而成为瓶颈。现代 CPUs 可以同时处理多个内存访问,因此您可以通过并行访问 4 个内存区域来额外压缩 ~5% 的性能,这就是 4 路循环所做的(对于您的特定数组4 种方式是最快的)。有关详细信息,请参阅 Memory-level parallelism: Intel Skylake versus Intel Cannonlake。
您可以创建一个结构(或 class)来保存数组中所有元素共有的参数。
struct nameIt
{
uint8_t* arr;
uint8_t delta;
}
您可以使用 std::for_each
:
uint8_t value = 15;
std::for_each(std::begin(nums), std::end(nums), [value](uint8_t& num) { num -= value; });
其中 nums 是 uint8_t.
的数组
这应该是最快的方法:
#include <iostream>
#include <cstdint>
#include <array>
#include <algorithm>
#include <execution>
int main() {
constexpr size_t size = 1824 * 942;
uint16_t input{};
std::cout << "Initialize with: ";
std::cin >> input;
std::array<uint8_t, size> array{};
std::fill(std::execution::par_unseq, array.begin(), array.end(), input);
std::transform(std::execution::par_unseq,array.begin(), array.end(), array.begin(), [] (const auto& value) { return value + 15; });
std::for_each(array.begin(),array.end(), [] (auto value) {
std::cout << static_cast<uint16_t>(value) << ",";
});
std::cout << "\n";
}
注意重要的一行std::transform(std::execution::par_unseq,array.begin(), array.end(), array.begin(), [] (const auto& value) { return value + 15; });
其余的只是为了举例。
另请注意,由于您没有指定哪种数组类型,您可以将 uint8_t array[1924*924];
等内置数组转换为具有 std::to_array
的 std::array
。
我有一个很大的 uint8_t
数组(大小 = 1824 * 942)。我想对每个元素做同样的操作。特别是我需要从每个元素中减去 -15。
这个数组每秒刷新 20 次,所以时间是个问题,我正在避免在数组上循环。
有没有简单的方法来做到这一点?
你可以只写一个带有普通循环的函数:
void add(uint8_t* a, size_t a_len, uint8_t b) {
for(uint8_t* ae = a + a_len; a < ae; ++a)
*a += b;
}
并希望编译器为您矢量化它,它确实做到了,请参阅 assembly。
std::for_each
和 std::transform
的解决方案,例如:
void add(uint8_t* a, size_t a_len, uint8_t b) {
std::transform(a, a + a_len, a, [b](auto value) { return value + b; });
}
应该生成完全相同的代码,但有时却不会。
[更新]
出于好奇,我对以下解决方案进行了基准测试:
#include <benchmark/benchmark.h>
#include <cstdint>
#include <array>
#include <algorithm>
#include <immintrin.h>
constexpr size_t SIZE = 1824 * 942;
alignas(32) std::array<uint8_t, SIZE> A;
__attribute__((noinline)) void add_loop(uint8_t* a, size_t a_len, uint8_t b) {
for(uint8_t* ae = a + a_len; a < ae; ++a)
*a += b;
}
__attribute__((noinline)) void add_loop_4way(uint8_t* a, size_t a_len, uint8_t b) {
a_len /= 4;
for(uint8_t* ae = a + a_len; a < ae; ++a) {
a[a_len * 0] += b;
a[a_len * 1] += b;
a[a_len * 2] += b;
a[a_len * 3] += b;
}
}
__attribute__((noinline)) void add_transform(uint8_t* a, size_t a_len, uint8_t b) {
std::transform(a, a + a_len, a, [b](auto value) { return value + b; });
}
inline void add_sse_(__m128i* sse_a, size_t a_len, uint8_t b) {
__m128i sse_b = _mm_set1_epi8(b);
for(__m128i* ae = sse_a + a_len / (sizeof *sse_a / sizeof b); sse_a < ae; ++sse_a)
*sse_a = _mm_add_epi8(*sse_a, sse_b);
}
__attribute__((noinline)) void add_sse(uint8_t* a, size_t a_len, uint8_t b) {
add_sse_(reinterpret_cast<__m128i*>(a), a_len, b);
}
inline void add_avx_(__m256i* avx_a, size_t a_len, uint8_t b) {
__m256i avx_b = _mm256_set1_epi8(b);
for(__m256i* ae = avx_a + a_len / (sizeof *avx_a / sizeof b); avx_a < ae; ++avx_a)
*avx_a = _mm256_add_epi8(*avx_a, avx_b);
}
__attribute__((noinline)) void add_avx(uint8_t* a, size_t a_len, uint8_t b) {
add_avx_(reinterpret_cast<__m256i*>(a), a_len, b);
}
template<decltype(&add_loop) F>
void B(benchmark::State& state) {
for(auto _ : state)
F(A.data(), A.size(), 15);
}
BENCHMARK_TEMPLATE(B, add_loop);
BENCHMARK_TEMPLATE(B, add_loop_4way);
BENCHMARK_TEMPLATE(B, add_transform);
BENCHMARK_TEMPLATE(B, add_sse);
BENCHMARK_TEMPLATE(B, add_avx);
BENCHMARK_MAIN();
i7-7700k CPU 和 g++-8.3 -DNDEBUG -O3 -march=native -mtune=native
上的结果:
------------------------------------------------------------------
Benchmark Time CPU Iterations
------------------------------------------------------------------
B<add_loop> 31589 ns 31589 ns 21981
B<add_loop_4way> 30030 ns 30030 ns 23265
B<add_transform> 31590 ns 31589 ns 22159
B<add_sse> 39993 ns 39992 ns 17403
B<add_avx> 31588 ns 31587 ns 22161
循环、转换和 AVX2 版本的时间几乎相同。
SSE 版本较慢,因为编译器生成更快的 AVX2 代码。
perf report
报告 ~50% L1d-cache 未命中率,这表明该算法因内存访问而成为瓶颈。现代 CPUs 可以同时处理多个内存访问,因此您可以通过并行访问 4 个内存区域来额外压缩 ~5% 的性能,这就是 4 路循环所做的(对于您的特定数组4 种方式是最快的)。有关详细信息,请参阅 Memory-level parallelism: Intel Skylake versus Intel Cannonlake。
您可以创建一个结构(或 class)来保存数组中所有元素共有的参数。
struct nameIt
{
uint8_t* arr;
uint8_t delta;
}
您可以使用 std::for_each
:
uint8_t value = 15;
std::for_each(std::begin(nums), std::end(nums), [value](uint8_t& num) { num -= value; });
其中 nums 是 uint8_t.
的数组这应该是最快的方法:
#include <iostream>
#include <cstdint>
#include <array>
#include <algorithm>
#include <execution>
int main() {
constexpr size_t size = 1824 * 942;
uint16_t input{};
std::cout << "Initialize with: ";
std::cin >> input;
std::array<uint8_t, size> array{};
std::fill(std::execution::par_unseq, array.begin(), array.end(), input);
std::transform(std::execution::par_unseq,array.begin(), array.end(), array.begin(), [] (const auto& value) { return value + 15; });
std::for_each(array.begin(),array.end(), [] (auto value) {
std::cout << static_cast<uint16_t>(value) << ",";
});
std::cout << "\n";
}
注意重要的一行std::transform(std::execution::par_unseq,array.begin(), array.end(), array.begin(), [] (const auto& value) { return value + 15; });
其余的只是为了举例。
另请注意,由于您没有指定哪种数组类型,您可以将 uint8_t array[1924*924];
等内置数组转换为具有 std::to_array
的 std::array
。