如何设置重复性数据,以便可以优化大多数数据?

时间:2019-02-17 16:18:19

标签: c gcc optimization

我需要对32 kbit宽的数据执行按位与运算。这些值之一是固定的位掩码。

我一次执行AND 32位运算。简化后,我的算法将如下所示:

(我将从此示例中删除内存管理,变量范围问题等)

#include <stdint.h>

const uint32_t mask[1024] = {
            0b00110110100101100111001011000111,
            0b10001110100101111010010100100100,
            0b11101010010000110001101010010101,
            0b10001110100101111010010100100100,
            (...) // 1019 more lines!
            0b00110110100101100111001011000111};

uint32_t answer[1024] = {0};
uint32_t workingdata = 0;
uint16_t i = 0;

int main(void)
{
    for (i=0; i<1024; i++)
    {
        workingdata = getnextdatachunk();
        answer[i] = workingdata & mask[i];
    }

    do_something_with_answer();

    return 0;
}

这就是问题:如果您查看示例位掩码,则mask [1] == mask [3]和mask [0] == mask [1023]。

在我实际的位掩码中,大多数值都是重复的;整个1024值数组中只有20个不同的值。另外,在我的最终应用程序中,我有16个不同的位掩码,每个掩码具有相似的内部重复。

我正在寻找一种很好的方法来避免必须存储和迭代大量不必要的数据。

我考虑过的一种方法类似于查找表,其中我的数组仅包含每个所需的位掩码块的单个实例:

const uint32_t mask[20] = {
            0b00110110100101100111001011000111,
            0b10001110100101111010010100100100,
            (...) // only 17 more lines!
            0b11101010010000110001101010010101};

uint32_t answer[1024] = {0};
uint32_t workingdata = 0;
uint16_t i = 0;

int main(void)
{
    for (i=0; i<1024; i++)
    {
        workingdata = getnextdata();

        switch(i)
        {
            // the mask indexes are precalculated:

            case 0:
                answer[i] = workingdata & mask[5];
                break;
            case 1:
                answer[i] = workingdata & mask[2];
                break;
            case 2:
                answer[i] = workingdata & mask[2];
                break;
            case 3:
                answer[i] = workingdata & mask[0];
                break;
            case (...): // 1020 more cases!
                (...);
                break;
            default:
        }
    }

    do_something_with_answer();

    return 0;
}

或者,使用更紧凑的switch语句:

switch(i)
{
    // the mask indexes are precalculated:

    case 0,3,4,5,18,35,67,(...),1019:
        answer[i] = workingdata & mask[0];
        break;
    case 1,15,16,55,89,91,(...),1004:
        answer[i] = workingdata & mask[1];
        break;
    case (...): // Only 18 more cases!
        (...);
        break;
    default:
}

这两种解决方案实际上都不清楚发生了什么,我真的想避免。

理想情况下,我想保留原始结构,并让gcc的优化程序删除所有不必要的数据。 如何使我的代码写得很好并且仍然有效?

2 个答案:

答案 0 :(得分:3)

让我们发明点系统,并假设从L1缓存中获取数据的成本为4点,从L2缓存中获取数据的成本为8点,不可预测的分支成本为12点。请注意,选择这些点是为了粗略地表示“平均但未知的80x86 CPU的周期”。

具有单个1024个条目表的原始代码每次迭代的总成本为4点(假设其执行频率足以影响性能,因此,假设数据使用频率足够高,可以存储在L1缓存中)。 / p>

使用switch语句,编译器将(希望-如果分支是性能方面的噩梦,则是一系列的)将其转换为跳转表并执行类似goto table[i];的操作,因此它可能算作从表中获取( 4分),然后是一个不可预测的分支(12分);或每次迭代总共16点。

请注意,对于64位代码,编译器生成的跳转表将为1024个条目,其中每个条目均为64位。该表的大小将是第一个选项的表的两倍(该表是1024个条目,每个条目为32位)。但是,许多CPU中的L1数据高速缓存都是64 KiB,因此64 KiB跳转表意味着进入L1数据高速缓存的任何其他内容(源数据进行AND运算,结果“答案”数据,CPU堆栈上的任何内容)导致(64字节或8个条目)跳转表被从缓存中逐出以腾出空间。这意味着有时您需要为“ L1错过,L2命中”付费。假设发生这种情况的时间为5%,那么每次迭代的实际成本最终为“(95 *(4 + 12)+ 5 *(8 + 12))/ 100 = 16.2”分。

鉴于您希望第一个选项的性能更好(“每次迭代16.2点”明显大于“每次迭代4点”),并且您希望第一个选项的可执行文件大小更好选项(即使不考虑case中每个switch的任何代码,一个32 KiB表的大小也是64 KiB表的一半),并且考虑到第一个选项更简单(更易于维护) )代码;我看不出为什么要使用第二个选项。

要优化此代码,我会尝试处理更大的代码。举一个简单的例子,您可以做这样的事情吗?

    uint64_t mask[512] = { ....

    uint64_t workingdata;
    uint64_t temp;

    for (i=0; i<512; i++)
    {
        workingdata = getnextdatachunk() << 32 | getnextdatachunk();
        temp = workingdata & mask[i];
        answer[i*2] = temp;
        answer[i*2+1] = temp >> 32;
    }

如果您可以做这样的事情,那么它(最多)可能会使性能提高一倍;但是,如果您可以做到“每次迭代64位,迭代次数减少一半”,那么您还可以使用SIMD内部函数进行“每次迭代128位,迭代次数达到四分之一”或“每次迭代256位,迭代次数达到八分之一”迭代”,并且可能使其速度提高近8倍。

当然,除此之外的步骤是缓冲足够的源数据,以提高使用多个线程(多个CPU)的效率(例如,以便可以有效地摊销同步成本)。如果将4个CPU并行运行,每个迭代执行256位运算,则(理论上最好的情况)将获得“比原来的1024个迭代快32倍的速度,而单个CPU版本则为32位”。

答案 1 :(得分:2)

我个人认为您的方法实际上取决于您的用例。您有2种不同的模式:

  • 如果运行速度很重要,请将数组作为整体保留在内存中(考虑到数组不会太大而不会弄乱缓存)。
  • 如果代码大小很重要,请使用您认为或建议的PSkocik之类的方法。

要选择合适的代码设计,您需要考虑很多不同的因素。例如,如果您的代码将在嵌入式设备上运行,则我可能会采用较小的代码大小方法。但是,如果您使用普通PC进行编码,那么我可能会选择第一个。