【问题标题】:Is it possible to optimize and faster multiplying an array by a number in C?是否可以优化和更快地将数组乘以 C 中的数字?
【发布时间】:2020-08-01 11:34:08
【问题描述】:

我有一个 C 代码,它将数组的每个元素乘以一个数字 (0-9),得到一系列以 10 为基数的数字。

我的问题是这个函数运行时间比我预期的要长。我需要它更快。 我知道在优化我的功能时我的问题是对进位的依赖。如何修改此代码以解决此问题并使代码更快? 解决方案可以使用内在函数或其他专门技术。

到目前为止我最快的版本是这样的:

void ConstMult( uint8_t *V, size_t N, uint8_t digit )
{
  uint8_t CARRY = 0;
  for ( size_t i=0; i< N; ++i )
  {
    V[i] = V[i] * digit + CARRY;
    CARRY = ((uint32_t)V[i] * (uint32_t)0xCCCD) >> 19;
    V[i] -= (CARRY << 3) + (CARRY << 1);
  }
}

但我也尝试了这些较慢的方法:

uint8_t ConstMult( uint8_t *V, size_t N, uint8_t digit )
{
  uint8_t CARRY = 0;
  for ( int i=0; i< N; i++ ) 
  {
    char R = V[i] * digit + CARRY;
    CARRY = R / 10;
    R = R - CARRY*10;
    V[i] = R;
  }
  return CARRY; // may be from 0 to 9
}
uint8_t ConstMult(uint8_t *V, size_t N, uint8_t digit)
{
  uint8_t CARRY = 0;
  uint8_t ja = 0;
  for (size_t i = 0; i < N; ++i) {
    uint8_t aux = V[i] * digit;
    uint8_t R = aux + CARRY;
    CARRY = ((u_int32_t)R*(u_int32_t)0xCCCD) >> 19;
    ja = (CARRY << 3) + 2*CARRY;
    R -= ja;
    V[i] = R;
  }
  return CARRY;
}

【问题讨论】:

标签: c arrays performance optimization multiplication


【解决方案1】:

这是另一个实现(比其他实现快得多):

void ConstMult4(uint8_t *V, size_t N, uint8_t digit)
{
    uint8_t CARRY = 0;

    const uint32_t coef7  = digit * 10000000;
    const uint32_t coef6  = digit * 1000000;
    const uint32_t coef5  = digit * 100000;
    const uint32_t coef4  = digit * 10000;
    const uint32_t coef3  = digit * 1000;
    const uint32_t coef2  = digit * 100;
    const uint32_t coef1  = digit * 10;
    const uint32_t coef0  = digit;

    static uint8_t table[10000][4];
    static int init = 1;

    if(init)
    {
        for(int i=0 ; i<10000 ; ++i)
        {
            table[i][0] = (i / 1) % 10;
            table[i][1] = (i / 10) % 10;
            table[i][2] = (i / 100) % 10;
            table[i][3] = (i / 1000) % 10;
        }

        init = 0;
    }

    for(size_t i=0 ; i<N/8*8 ; i+=8)
    {
        const uint32_t val = V[i+7]*coef7 + V[i+6]*coef6 + V[i+5]*coef5 + V[i+4]*coef4 + V[i+3]*coef3 + V[i+2]*coef2 + V[i+1]*coef1 + V[i+0]*coef0 + CARRY;

        CARRY = val / 100000000;

        const uint32_t loVal = val % 10000;
        const uint32_t hiVal = val / 10000 - CARRY * 10000;
        const uint8_t* loTablePtr = &table[loVal][0];
        const uint8_t* hiTablePtr = &table[hiVal][0];

        // Assume the compiler optimize the 2 following calls
        // (otherwise the performance could be quite bad).
        // memcpy is used to prevent performance issue due to pointer aliasing. 
        memcpy(V+i, loTablePtr, 4);
        memcpy(V+i+4, hiTablePtr, 4);
    }

    for(size_t i=N/8*8 ; i<N ; ++i)
    {
        V[i] = V[i] * digit + CARRY;
        CARRY = V[i] / 10;
        V[i] -= CARRY * 10;
    }
}

此实现假定Vdigit 中的计算数字实际上是数字。 它明显快于其他方法:

  • 按照@phuclv 的建议在内部使用更大的基础(它减少了关键路径并引入了更多并行性);
  • 使用@chqrlieforyellowblockquotes 提出的查找表(它可以非常快速地计算除法/模运算)。

甚至可以通过使用 SSE 4.1 内部函数(SIMD 指令)来改进此代码。但是以较少可移植的代码为代价(尽管它可以在大多数基于 x86_64 的现代处理器上工作)。这是实现:

void ConstMult5(uint8_t *V, size_t N, uint8_t digit)
{
    uint8_t CARRY = 0;

    static uint8_t table[10000][4];
    static int init = 1;

    if(init)
    {
        for(int i=0 ; i<10000 ; ++i)
        {
            table[i][0] = (i / 1) % 10;
            table[i][1] = (i / 10) % 10;
            table[i][2] = (i / 100) % 10;
            table[i][3] = (i / 1000) % 10;
        }

        init = 0;
    }

    __m128i coefs1 = _mm_set_epi16(1000, 100, 10, 1, 1000, 100, 10, 1);
    __m128i coefs2 = _mm_set_epi32(10000*digit, 10000*digit, digit, digit);

    for(size_t i=0 ; i<N/16*16 ; i+=8)
    {
        // Require SSE 4.1 (thus smmintrin.h need to be included)
        const __m128i vBlock = _mm_loadu_si128((const __m128i*)&V[i]); // load 16 x uint8_t values (only half is used)
        const __m128i v = _mm_cvtepu8_epi16(vBlock); // Convert the block to 8 x int16_t values
        const __m128i tmp1 = _mm_madd_epi16(v, coefs1); // Compute the sum of adjacent pairs of v * coefs1 and put this in 4 x int32_t values
        const __m128i tmp2 = _mm_add_epi32(tmp1, _mm_shuffle_epi32(tmp1, 0b10110001)); // Horizontal partial sum of 4 x int32_t values
        const __m128i tmp3 = _mm_mul_epu32(tmp2, coefs2); // Compute tmp2 * coefs2 and put this in 2 x int64_t values
        const uint32_t val = _mm_extract_epi64(tmp3, 1) + _mm_extract_epi64(tmp3, 0) + CARRY; // Final horizontal sum with CARRY

        CARRY = val / 100000000;

        const uint32_t loVal = val % 10000;
        const uint32_t hiVal = val / 10000 - CARRY * 10000;
        const uint8_t* loTablePtr = &table[loVal][0];
        const uint8_t* hiTablePtr = &table[hiVal][0];

        // See the memcpy remark in the code above (alternative version).
        memcpy(V+i, loTablePtr, 4);
        memcpy(V+i+4, hiTablePtr, 4);
    }

    for(size_t i=N/16*16 ; i<N ; ++i)
    {
        V[i] = V[i] * digit + CARRY;
        CARRY = V[i] / 10;
        V[i] -= CARRY * 10;
    }
}

以下是我的机器(使用 i7-9700KF 处理器)上的性能结果(使用随机输入重复运行 1000 次并取平均值):

ConstMult0(10000): 11.702 us
ConstMult3(10000): 6.768 us (last optimized version)
ConstMult4(10000): 3.569 us
ConstMult5(10000): 2.552 us

基于 SSE 的最终版本比您的原始实现快 4.6 倍!

【讨论】:

  • 感谢您的回复是一种非常有趣的优化方式。但是,在 C 中运行时,我没有 bool 变量,也没有使用 std:copy 的选项。我知道它是一个 C++ 版本,但我怎么能把它转换成 C 代码呢?
  • 如何转换行:std::copy(loTablePtr, loTablePtr+4, V+i);标准::复制(hiTablePtr,hiTablePtr+4,V+i+4);为了能够在 C 中运行代码?
  • 抱歉,我想念您使用的是 C 而不是 C++。所以,我将 C++ 部分翻译成 C。
  • 谢谢,你的优化很有趣。
  • 您打算使用更大的底座吗?如果不是,您可以在标量版本中将 memcpy 调用替换为普通循环(对于 SSE 版本,代码不支持)。如果是,我建议您使用 2 的幂,因为这样代码会更快(如果基数足够大,甚至可能是原始代码)。在这种情况下,您不再需要查找表。如果您需要使用不是 2 的幂且非常大的基数,则表查找优化不再起作用(在这种情况下我看不到任何优化代码的方法)。
【解决方案2】:

这是一个使用辅助表一次处理 2 个字节的块而不进行分割的函数:

uint8_t ConstMult3(uint8_t *V, size_t N, uint8_t digit) {
#define TABLE_SIZE  ((9 * 256 + 9) * 9 + 9 + 1)
    static uint32_t table[TABLE_SIZE];
    if (!table[1]) {
        for (uint32_t x = 0; x < TABLE_SIZE; x++) {
            uint32_t u = x % 256 % 10;
            uint32_t d = (x / 256 + x % 256 / 10) % 10;
            uint32_t c = (x / 256 + x % 256 / 10) / 10;
            //table[x] = u | (d << 8) | (c << 16);
            // modified following Jerome Richard's comment
            table[x] = c | (u << 8) | (d << 16);
        }
    }
    if (N == 0 || digit <= 1) {
        if (digit == 0)
            memset(V, 0, N);
        return 0;
    } else {
        size_t CARRY = 0;

        if ((uintptr_t)V & 1) {  // V is misaligned
            int R = V[0] * digit + (uint8_t)CARRY;
            CARRY = (uint8_t)(R / 10);
            V[0] = (uint8_t)(R - CARRY * 10);
            V++;
            N--;
        }
        {   // handle aligned block 2 bytes at a time
            uint16_t *V2 = (uint16_t *)(void *)V;
            size_t N2 = N / 2;
            for (size_t i = 0; i < N2; i++) {
                uint32_t x = table[V2[i] * digit + CARRY];
                //V2[i] = (uint16_t)x;
                //CARRY = x >> 16;
                // modified following Jerome Richard's comment
                V2[i] = (uint16_t)(x >> 8);
                CARRY = (uint8_t)x;
            }
        }
        if (N & 1) {    // handle last byte
            int R = V[N - 1] * digit + (uint8_t)CARRY;
            CARRY = (uint8_t)(R / 10);
            V[N - 1] = (uint8_t)(R - CARRY * 10);
        }
        return (uint8_t)CARRY;
    }
#undef TABLE_SIZE
}

在我的慢速笔记本电脑上,在 64 位模式下使用 clang 9.0,我得到这些计时,ConstMult0ConstMult1ConstMult2 是问题中发布的函数:

ConstMult0(1000000): 15.816ms sum0=4495507, sum=4501418 ConstMult1(1000000): 16.464ms sum0=4495507, sum=4501418 ConstMult2(1000000): 16.483ms sum0=4495507, sum=4501418 ConstMult3(1000000): 9.644ms sum0=4495507, sum=4501418

编辑:在 Jérôme Richard 的评论之后,表格内容的微小变化可将性能提高 11%:

ConstMult0(1000000): 15.837ms sum0=4500384, sum=4495487 ConstMult1(1000000): 16.494ms sum0=4500384, sum=4495487 ConstMult2(1000000): 16.482ms sum0=4500384, sum=4495487 ConstMult3(1000000): 8.537ms sum0=4500384, sum=4495487

【讨论】:

  • 计算受限于关键路径上的指令操作CARRY(顺序依赖链)。您的解决方案令人印象深刻,但可以通过再次减少关键路径来优化。我建议您使用table[x] = c | (u &lt;&lt; 8) | (d &lt;&lt; 16); 行来填充表格,并在主计算循环中使用V2[i] = x &gt;&gt; 8; CARRY = (uint8_t)x; 这两行。这在我的机器上快了大约 10%,并且得到了相同的结果。
  • @JérômeRichard:很好的建议!实际上,我考虑过这种替代方案,并认为如果不进行测试,它不会有太大的不同。如此卑微的经历。优化时,不要假设任何东西,仔细进行基准测试,让时钟成为裁判。
  • @Des 乘以 10 通常是通过移位和加法来实现的,LEA 在 x86 架构上,与让编译器优化相比,显式执行它可以获得更好的时序,这令人惊讶。请注意,在您的第二个函数中将 char R 更改为 int R 在我的 Mac 上提高了 10% 以上的性能。
  • @Des:我不知道如何解释这条消息。提高效率的真正途径是改变大数字的内部表示。为每个十进制数字使用一个字节非常缓慢。将十进制数字打包成每 32 位字 9 个一组会更好,如果在计算最终结果之前有多个步骤,使用二进制表示将进一步提高整体效率。如果使用ConstMult实现bignum乘法中的内循环,则实际上不需要传播进位,直到将所有中间步骤相加得到最终结果。
  • @Des:不传播进位应该允许编译器展开循环并向量化代码。如果我删除进位传播并在内循环中写入V[i] *= digit;,则时间会下降到0.583ms。使用 16 位中间存储将允许您将 V1[i] += V[i] * digit; 作为内部操作而无需进行进位传播,并且仅在最终结果上最终减少为一位十进制数字。整体性能提升 10 到 20 倍。打包数字会在此之上增加 5 到 10 倍。更大的图景是什么?这个函数是干什么用的?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-08
  • 2016-07-01
  • 2021-03-05
相关资源
最近更新 更多