【问题标题】:How to optimize the KASUMI algorithm S-boxes?如何优化 KASUMI 算法的 S-boxes?
【发布时间】:2018-01-24 00:58:52
【问题描述】:

非常感谢,我正在尝试优化用C编写的Kasumi算法。FI函数中有S-box用于加密数据,S7-box有127个元素,S9-box有512个元素。 FI函数代码如下:

static u16 FI(u16 in, u16 subkey)
{
    static u16 s7[] = {...};
    static u16 s9[] = {...};

    nine = (u16)(in>>7);
    seven = (u16)(in&0x7F);
    /* Now run the various operations */
    nine = (u16)(S9[nine] ^ seven);
    seven = (u16)(S7[seven] ^ (nine & 0x7F));
    seven ^= (subkey>>9);
    nine ^= (subkey&0x1FF);
    nine = (u16)(S9[nine] ^ seven);
    seven = (u16)(S7[seven] ^ (nine & 0x7F));
    in = (u16)((seven<<9) + nine);
    return( in );
}

u16 表示无符号短。

通过一些转换。我将 S7-box 和 S9-box 合并到 S16-box,并使用 avx 指令使 16 个数据并行。 FI函数代码如下:

static u16 FI(__m256i in, u16 subkey)
{
    u16 arr[16];        
    _mm256_store_si256((__m256i*)arr, in);
    u8 i;           
    for(i = 0; i < 16; i++)
    {
        arr[i] = (u16)(s16[arr[i]] ^ subkey);
        arr[i] = (arr[i] << 7) | (arr[i] >> 9);
        arr[i] = s16[arr[i]];
    }
    in = _mm256_load_si256((__m256i*)arr);
}

S16-box 有 65536 个元素,所以可能会发生一些缓存未命中。我还使用收集指令,例如:

inline static __m256i FI( __m256i in, u16 subkey )
{
    __m256i _tmp = _mm256_set1_epi32(0xffff);
    __m256i even_sequence = _mm256_and_si256(in, _tmp);
    __m256i odd_sequence = _mm256_srli_epi32(in, 16);
    even_sequence = _mm256_i32gather_epi32((int const*)s16, even_sequence, 2); 
    __m256i _subkey = _mm256_set1_epi16(subkey);
    even_sequence = _mm256_xor_si256(even_sequence, _subkey);
    even_sequence = _mm256_and_si256(even_sequence, _tmp);
    odd_sequence = _mm256_i32gather_epi32((int const*)s16, odd_sequence, 2); 
    odd_sequence = _mm256_xor_si256(odd_sequence, _subkey);
    odd_sequence = _mm256_and_si256(odd_sequence, _tmp);
    // rotate
    __m256i hi = _mm256_slli_epi16(even_sequence, 7); 
    __m256i lo = _mm256_srli_epi16(even_sequence, 9); 
    even_sequence = _mm256_or_si256(hi, lo);
    //same for odd
    hi = _mm256_slli_epi16(odd_sequence, 7); 
    lo = _mm256_srli_epi16(odd_sequence, 9); 
    odd_sequence = _mm256_or_si256(hi, lo);
    even_sequence = _mm256_i32gather_epi32((int const*)s16, even_sequence, 2); 
    odd_sequence = _mm256_i32gather_epi32((int const*)s16, odd_sequence, 2); 
    even_sequence = _mm256_and_si256(even_sequence, _tmp);
    odd_sequence = _mm256_slli_epi32(odd_sequence, 16);
    in = _mm256_or_si256(even_sequence, odd_sequence);  

    return in; 
}

但是性能不能满足要求,我也考虑bit-slice。我读了一篇论文,它可以并行处理 128 个数据,但需要一些硬件支持。我认为位转置操作很耗时并且有很多限制。

非常感谢!

【问题讨论】:

  • 使用同一个子密钥加密多少字节/位? (您建议使用 16,但 Kasumi AFAIK 以 8 字节块加密数据?)添加 sbox 生成器功能怎么样?
  • 那么这三个变体都是等价的吗?您是否对它们进行了描述,您能否分享一些数字,它们之间的差异有多大,或者最终大部分时间都花在了哪里?或者如果可能(足够短),添加一些初始化代码以使代码为minimal reproducible example,这样可以自己尝试,但仍然添加一些关于您当前位置的上下文(您离要求有多少)会很好。跨度>
  • @Bai,你能添加一些 L1 缓存未命中的数量吗?
  • 使用 C11 _Alignas(32) u16 arr[16]; 确保 256b 存储不会出错。 (如果还没有,也许您正在使用将_mm256_store_si256 编译为vmovdqu 的编译器。某些编译器(如gcc)会将其编译为vmovdqa,因此它会在未对齐时出错,而不是可能运行得更慢(例如,对于缓存行拆分)。我想这只是您的参考实现,而不是您正在优化的内容,但在收集速度较慢的 CPU 上可能会更好。使用 _mm_cvtsi128_si32 执行前两个元素(并使用标量掩码/移位),因此您可以从较低的延迟开始。

标签: c assembly encryption optimization


【解决方案1】:

这段代码可能会解释性能问题以及您在其下方的评论。

static u16 FI(__m256i in, u16 subkey) {
    u16 arr[16];        
    _mm256_store_si256((__m256i*)arr, in);
    u8 i;           
    for(i = 0; i < 16; i++)
    {
        arr[i] = (u16)(s16[arr[i]] ^ subkey);
        arr[i] = (arr[i] << 7) | (arr[i] >> 9);
        arr[i] = s16[arr[i]];
    }
    in = _mm256_load_si256((__m256i*)arr);
}

S16-box 有 65536 个元素,所以可能会发生一些缓存未命中。

平均 x64 处理器只有 32KB 的 L1(AMD 有时有 64K,但现在让我们忽略它)。

这意味着使用随机访问模式,如果没有其他数据结构使用任何 L1 并且您没有运行也可以使用一些 L1 的超线程,您的 64K 阵列将获得 32KB/64KB * 100% = 50% 的缓存命中率另一个线程。

让我们将其简化为说您只有 64KB 中的 16KB,从而为每次访问提供 75% 的失败机会。所以你的循环在每一行之间都有数据依赖关系,即。在上一个语句完成之前,下一个语句不能开始。幸运的是,每次迭代都是独立于其他迭代的数据。

arr[i] = (u16)(s16[arr[i]] ^ subkey);
arr[i] = (arr[i] << 7) | (arr[i] >> 9);
arr[i] = s16[arr[i]];

arr 此时几乎肯定会在 L1 缓存中,只产生 4 个周期的启动成本,每次访问 s16 平均会花费 0.25*4+0.75*12 = 1+9 = 10 个周期。这给出了以下每个语句的近似延迟成本(忽略存储和重新加载 arr[i] 的成本,假设 arr[i] 存储在寄存器中)

arr[i] = (u16)(s16[arr[i]] ^ subkey); // arr: 4 + S16: 10 + ^:1
arr[i] = (arr[i] << 7) | (arr[i] >> 9); // << : 1 + |: 1
arr[i] = s16[arr[i]]; // s16 : 10 + store arr : 4

每次迭代有 31 个周期的延迟,幸运的是每次迭代之间没有数据依赖关系。每次迭代大约需要 3 个周期来发布,因此假设完美的分支预测并忽略最后分配 in 的数据冒险,最后一次将在 ~3*16+31=79 个周期内完成。

我认为你的下一个代码是这个循环重写为 AVX2 将有很多相同的负载依赖关系和完全相同的缓存未命中,循环开销将消失,但一些较长延迟的 AVX 指令可能会增加时间。平均时间仍然是约 31 个周期延迟 + 一些 AVX 延迟 + 16 个负载/(每个周期最多 2 个负载),比如说 40 个周期。

如果您没有合并 S7 和 S9,它们只会占用 (128+512)*2 字节,并且当您运行更长的编码时,几乎可以肯定它们总是在 L1 缓存中。然后循环延迟将下降到一半,代价是负载数量增加一倍,整个 AVX 达到每个周期 15 + 32 负载 / 2,比如说 30 个周期。

好消息是每个 16 字节的迭代似乎都独立于前一个,因此它们可以在时间上重叠。但是您最终会受到加载和存储数量的限制,一个初始加载,来自 s7+s9 的 32 个加载和一个存储,最多 2 个存储或加载将最佳吞吐量限制为 16 字节/((1+32+ 1)/2) 个周期。

这是做了很多乐观的假设,只有对 2 个不同代码(s16 与 s7+s9)的实际测量才能决定什么是最好的。

【讨论】:

  • 很好地分析了缓存未命中问题。但是当你描述它时,你混淆了延迟和吞吐量。当乱序执行可以隐藏延迟时,延迟不是问题。这里的问题不是直接延迟,因为每次迭代都是独立的。我认为有限的内存并发将是这里真正的瓶颈;例如,Skylake CPU 只有 10 个行填充缓冲区来跟踪 L1D 缓存行的未完成请求。因此,您不能保持与此代码生成的几乎一样多的缓存未命中。 (另见Latency Bound
  • 31 个周期的延迟将在 ROB 中填满,从而降低吞吐量。 10 个杰出的限制只会让情况变得更糟。
  • OP没有说什么硬件,但即使是Sandybridge也有168项ROB。 (和一个 160 条目的整数 PRF)。但是,是的,ROB 和/或 RS 将在缓存未命中延迟中填满,然后才能在运行中获得足够的迭代以实现每个时钟吞吐量 2 次负载。但请记住,这是一个平均延迟,当一个迭代停止时,其他迭代中的未完成负载可以取得进展。
  • 无论如何,很多缓存未命中都会很糟糕,但假设延迟 = 平均值的简单分析可能无法准确告诉您瓶颈在哪个微架构资源上。
  • 非常感谢,我试过用s7+s9,可以减少失手率。但根据我的实验,结果变得更糟。也许在我的代码的其他部分,我使用 Avx 指令。
猜你喜欢
  • 2017-05-12
  • 2019-10-17
  • 2016-12-05
  • 2011-03-05
  • 1970-01-01
  • 1970-01-01
  • 2021-11-03
  • 2016-07-30
相关资源
最近更新 更多