减去或添加常数到大型数组

Iva*_*van 3 c++ arrays math pointer-arithmetic integer-arithmetic

我有一个大型uint8_t数组(大小= 1824 * 942)。我想对每个元素执行相同的操作。特别是我需要从每个元素中减去-15。

该阵列每秒刷新20次,因此时间是个问题,我避免在阵列上循环。

是否有捷径可寻?

Max*_*kin 5

您可以使用一个简单的循环编写一个函数:

void add(uint8_t* a, size_t a_len, uint8_t b) {
    for(uint8_t* ae = a + a_len; a < ae; ++a)
        *a += b;
}
Run Code Online (Sandbox Code Playgroud)

并希望编译器为您进行矢量化处理(请参见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; });
}
Run Code Online (Sandbox Code Playgroud)

应该生成完全相同的代码,但有时却不会。


[更新]

出于好奇,我对以下解决方案进行了基准测试:

#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();
Run Code Online (Sandbox Code Playgroud)

在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
Run Code Online (Sandbox Code Playgroud)

循环,转换和AVX2版本的时间几乎相同。

SSE版本较慢,因为编译器生成的AVX2代码更快。

perf report报告大约50%的一级缓存未命中率,这表明该算法已成为内存访问的瓶颈。现代的CPU可以同时处理多个内存访问,因此您可以通过并行访问4个内存区域来压缩这里约5%的性能,这就是4路循环所做的(对于您特定的数组大小,4路是最快的)。有关更多详细信息,请参阅内存级并行性:Intel Skylake与Intel Cannonlake