c++ 如何改进大型uint64数组的XOR?

zqdjd7g9  于 11个月前  发布在  其他
关注(0)|答案(2)|浏览(83)

我想对大的移位数组进行异或运算,下面是该函数的可移植版本,以便于解释。我如何改善这种计算?我已经尝试使用AVX 2,但没有看到太大的改善。目前,在下面的例子中显示的数据库需要50毫秒来处理所有内容,这是12 GB/s,我将感谢任何改善计算的提示。

#include <iostream>

uint64_t partition_size = 4096;
uint64_t entry_size = 32; // bytes
uint64_t DB_size = 16777216;
uint64_t *DB = new uint64_t[DB_size * entry_size/64];

//partition_index will be a random multiple of partition_size, e.g. 0, 8192, 4096 etc
//random_offset will be a random number in [0, partition_size]
void xor_shifted_arrays(uint32_t partition_index, uint32_t random_offset, uint64_t *result)
{
    auto uint64_per_entry = entry_size / sizeof(uint64_t);

    int shift_offset;
    uint32_t shift;
    
    for (int i = 0; i < partition_size  ; i = i + 1)
    {
        shift = (i + random_offset) & (partition_size - 1);
        shift_offset = shift * uint64_per_entry;
        
        for (int j = 0; j < uint64_per_entry; j=j+1){
            result[shift_offset + j] = result[shift_offset + j] ^ DB[partition_index + j];  
        }
        partition_index = partition_index + uint64_per_entry;
    }
}

字符串
更新:这里是godbolt:https://godbolt.org/z/j14av3fGq也在两个示例上运行了这段代码。
英特尔(R)酷睿(TM)i7- 7700 K CPU@4.20 GHz,运行MacOS 13.6,16 GB DDR4 RAM,编译器Apple clang版本15.0.0(clang-1500.0.40.1)
AWS r7i.2xlarge Intel(R)Xeon(R)Platinum 8488C with Ubuntu,64 GB DDR5 RAM,compiler g++(Ubuntu 11.4.0-1ubuntu1~22.04)11.4.0
令人惊讶的是,它在Xeon上慢了2倍!!
我正在使用编译器标志O3
Update 2:我觉得这可能是有用的,上面的代码从外部函数调用类似这样的东西(不是运行代码)

void outer_function(){
  uint64_t *result1 = new uint64_t[partition_size];
  uint64_t *result2 = new uint64_t[partition_size];
  uint64_t number_partitions = 4096;
  for (int i=0; i< number_partitions; i++){
      xor_shifted_arrays(i*partition_size,           some_rnd_gen(), result1)
  }
  for (int i=0; i< number_partitions; i++){
     xor_shifted_arrays(i*partition_size,                some_rnd_gen(), result2)
  }
}

hxzsmxv2

hxzsmxv21#

这些大多是通用的提示,这将帮助您的编译器优化代码(其中许多已经在注解中):

  • 对于数组索引/大小,始终使用size_t(或者如果您希望允许负索引,则使用ptrdiff_t)。这将保存编译器,使其不必处理潜在的回绕或溢出行为。
  • 在初始化变量时/在使用变量的范围内,
  • 将所有在初始化后从未改变的变量Declare为const。如果编译器知道它们的值,它可以优化与它们的乘法或除法运算,特别是如果它们是2的幂。
  • 将您修改的数组指针作为函数参数传递给__restrict关键字(只有在您确定此数组不会与其他数组别名/重叠时才这样做)。这通常有助于编译器展开和SIMD优化。
  • 使用-march=native(或-march=target_architecture)编译代码,以优化本地或某些特定的CPU架构(当然,除了-O3)。
  • 只是为了可读性:而不是A = A + B,写A += B,(与^/^=等相同)。这通常对编译器无关紧要。

假设你的godbolt-link中的j=j+4是一个错别字,你的意思是j+=1(或++j),这会导致非常干净的AVX 2代码:

// assuming these are known at compile-time:
const size_t partition_size = 4096; 
const size_t entry_size = 256; // bits
const size_t DB_size = 16777216;

uint64_t *DB = new uint64_t[DB_size * entry_size/64];
const size_t uint64_per_entry = entry_size / sizeof(uint64_t);
//uint64_t *result = new uint64_t[partition_size]; // pass this as parameter

//partition_index will be a random multiple of partition_size, e.g. 0, 8192, 4096 etc
//random_offset will be a random number in [0, partition_size]
void xor_shifted_arrays(size_t partition_index, size_t random_offset, uint64_t *__restrict result)
{

    for (size_t i = 0; i < partition_size  ; i++)
    {
        const size_t shift = (i + random_offset) & (partition_size - 1);
        const size_t shift_offset = shift * uint64_per_entry;
        
        for (size_t j = 0; j < uint64_per_entry; j++){
            result[shift_offset + j] ^= DB[partition_index + j];    
        }
        partition_index += uint64_per_entry;
    }
}

字符串
Godbolt-Link:https://godbolt.org/z/Khs5aTPaK
我看不出这段代码有什么进一步的改进。你需要对每个result条目和相应的DB条目进行一次读写。如果你能控制内存管理,你可以确保指针与页面大小对齐(或者至少与你的SIMD宽度对齐)。
另外,如果(对于不同的问题)某些条目将被多次读取或修改,您可以尝试重新安排循环,以便减少变量从内存读取或写入内存的频率。

csga3l58

csga3l582#

OpenCL可以轻松地对简单代码进行向量化,并将工作分配给所有内核。(12核),在双通道4800 MHz DDR5内存上提供约44 GB/s带宽(这只是一个简单的所有元素的XOR,不完全是你的算法,所以它没有有效地使用缓存,而是简单地流数据):

int main()
{
    constexpr int N = 4096 * 4096;
    constexpr int numWorkers = 4096*32;
    GPGPU::Computer computer(GPGPU::Computer::DEVICE_CPUS);
    std::string nStr = std::string("constant int N = ") + std::to_string(N) + R"(;
    )";
    std::string nwStr = std::string("constant int stride = ") + std::to_string(numWorkers) + R"(;
    )";
    computer.compile(nStr+nwStr+R"(
    
        kernel void Xor(global unsigned long * dataIn, global unsigned long * dataOut)
        { 
            int id = get_global_id(0);
            unsigned long xorData = dataIn[id];
            for(int i=1;i<N/stride;i++)
            {
                xorData = xorData ^ dataIn[id + i*stride];
            }

            // reduced result
            dataOut[id]=xorData;
        })", "Xor");

    GPGPU::HostParameter dataIn = computer.createArrayInput<uint64_t>("dataIn",N);
    GPGPU::HostParameter dataOut = computer.createArrayOutput<uint64_t>("dataOut", numWorkers);
    auto parameters = dataIn.next(dataOut);

    uint64_t result = 0;
    size_t t;
    for (int rep = 0; rep < 20; rep++)
    {
        for (int init = 0; init < N; init++)
            dataIn.access<uint64_t>(init) = init;
        {
            GPGPU::Bench bench(&t);
            computer.compute(parameters, "Xor", 0, numWorkers, 256);

            // final pass on host-side with just numWorkers elements
            uint64_txorData = dataOut.access<uint64_t>(0);
            for (int i = 1; i < numWorkers; i++)
            {
                xorData ^= dataOut.access<uint64_t>(i);
            }
            result = xorData;
        }
        std::cout << "computation took " << t / 1000000000.0 << "s" << std::endl;
        std::cout << "xor: " << result << std::endl;
        std::cout << "------------" << std::endl;
    }
    return 0;
}

字符串
产出:

computation took 0.0079244s
xor: 0
------------
computation took 0.0030533s
xor: 0
------------
computation took 0.0030468s
xor: 0
------------
computation took 0.0031714s
xor: 0
------------
computation took 0.0030102s
xor: 0
------------
computation took 0.0030884s
xor: 0
------------
computation took 0.0029352s
xor: 0
------------
computation took 0.0029854s
xor: 0
------------
computation took 0.0029936s
xor: 0
------------
computation took 0.0029326s
xor: 0
------------
computation took 0.0030838s
xor: 0
------------
computation took 0.0031311s
xor: 0
------------
computation took 0.0030022s
xor: 0
------------
computation took 0.0031073s
xor: 0
------------
computation took 0.0029577s
xor: 0
------------
computation took 0.0030004s
xor: 0
------------
computation took 0.0031038s
xor: 0
------------
computation took 0.0029255s
xor: 0
------------
computation took 0.002938s
xor: 0
------------
computation took 0.0029927s
xor: 0
------------


我不得不安装英特尔的GPU才能使用CPU。我也试过GPU,但PCIE v4.0有一些瓶颈(只有32 GB/s左右,在CPU缓存之外)。
要控制L2和L3上的缓存或显式选择核心,您可以使用OpenCL的设备裂变功能。如果您不需要OpenCL,您仍然可以观察程序二进制的汇编输出,以了解它如何将代码向量化,并将其用于您自己的C++手工调优的AVX库。

相关问题