使用intrinsics优化SSH AES-CTR中XOR运算性能的技术咨询
我有一段代码,会遍历两个缓冲区,将二者做XOR运算后的结果存入第三个缓冲区,具体来说,两个输入分别是数据缓冲区和密钥流缓冲区,第三个为目标缓冲区。
涉事的完整函数如下:
static int ssh_aes_ctr(EVP_CIPHER_CTX *ctx, u_char *dest, const u_char *src, LIBCRYPTO_EVP_INL_TYPE len) { typedef union { #ifdef CIPHER_INT128_OK __uint128_t *u128; #endif uint64_t *u64; uint32_t *u32; uint8_t *u8; const uint8_t *cu8; uintptr_t u; } ptrs_t; ptrs_t destp, srcp, bufp; uintptr_t align; struct ssh_aes_ctr_ctx_mt *c; struct kq *q, *oldq; int ridx; u_char *buf; if (len == 0) return 1; if ((c = EVP_CIPHER_CTX_get_app_data(ctx)) == NULL) return 0; q = &c->q[c->qidx]; ridx = c->ridx; /* src already padded to block multiple */ srcp.cu8 = src; destp.u8 = dest; do { /* do until len is 0 */ buf = q->keys[ridx]; bufp.u8 = buf; /* figure out the alignment on the fly */ #ifdef CIPHER_UNALIGNED_OK align = 0; #else align = destp.u | srcp.u | bufp.u; #endif /* xor the src against the key (buf) * different systems can do all 16 bytes at once or * may need to do it in 8 or 4 bytes chunks * worst case is doing it as a loop */ #ifdef CIPHER_INT128_OK if ((align & 0xf) == 0) { destp.u128[0] = srcp.u128[0] ^ bufp.u128[0]; } else #endif /* 64 bits */ if ((align & 0x7) == 0) { destp.u64[0] = srcp.u64[0] ^ bufp.u64[0]; destp.u64[1] = srcp.u64[1] ^ bufp.u64[1]; /* 32 bits */ } else if ((align & 0x3) == 0) { destp.u32[0] = srcp.u32[0] ^ bufp.u32[0]; destp.u32[1] = srcp.u32[1] ^ bufp.u32[1]; destp.u32[2] = srcp.u32[2] ^ bufp.u32[2]; destp.u32[3] = srcp.u32[3] ^ bufp.u32[3]; } else { /*1 byte at a time*/ size_t i; for (i = 0; i < AES_BLOCK_SIZE; ++i) dest[i] = src[i] ^ buf[i]; } /* inc/decrement the pointers by the block size (16)*/ destp.u += AES_BLOCK_SIZE; srcp.u += AES_BLOCK_SIZE; /* Increment read index, switch queues on rollover */ if ((ridx = (ridx + 1) % KQLEN) == 0) { oldq = q; /* Mark next queue draining, may need to wait */ c->qidx = (c->qidx + 1) % numkq; q = &c->q[c->qidx]; pthread_mutex_lock(&q->lock); while (q->qstate != KQFULL) { pthread_cond_wait(&q->cond, &q->lock); } q->qstate = KQDRAINING; pthread_cond_broadcast(&q->cond); pthread_mutex_unlock(&q->lock); /* Mark consumed queue empty and signal producers */ pthread_mutex_lock(&oldq->lock); oldq->qstate = KQEMPTY; pthread_cond_broadcast(&oldq->cond); pthread_mutex_unlock(&oldq->lock); } } while (len -= AES_BLOCK_SIZE); c->ridx = ridx; return 1; }
我使用vtune对代码进行性能分析后发现,destp.u128[0] = srcp.u128[0] ^ bufp.u128[0]; 这行代码占用了4%的CPU运行时间。
该行对应的汇编代码如下:
shl $0x4, %rax addq (%rsp), %rax movq (%rax), %rdx xorq (%rbx), %rdx movq 0x8(%rax), %rax xorq 0x8(%rax), %rax movq %rax, 0x8(%r12)
其中xorq (%rbx), %rdx指令就消耗了3.5%的CPU时间。我想知道能否通过对需要做XOR运算的数据做向量化处理,再使用intrinsics执行XOR操作来提升性能。我几乎没有使用intrinsics的实际经验,但愿意学习相关技术。我不确定现有代码是否已经接近预期的最优水平,希望能获得相关指导建议,谢谢。
现有代码的性能瓶颈原因
你当前的__uint128_t写法理论上是128位运算,但编译器默认会将其拆分为两条64位的加载、异或、存储指令,没有利用CPU内置的SIMD向量运算单元,这是你看到两条xorq指令、且占比较高的核心原因。
可以通过SIMD intrinsics获得明确性能提升
XOR是非常适合向量化的运算,对于x86架构,你可以用基线支持的SSE2指令集完成128位向量异或,不需要依赖高端指令集扩展,优化步骤如下:
- 引入SSE2头文件:
#include <emmintrin.h>
- 修改
ptrs_t联合体,增加SIMD指针类型:
typedef union { #ifdef CIPHER_INT128_OK __uint128_t *u128; #endif __m128i *u128_simd; // 新增SSE2 128位向量指针 uint64_t *u64; uint32_t *u32; uint8_t *u8; const uint8_t *cu8; uintptr_t u; } ptrs_t;
- 替换原
__uint128_t分支的运算逻辑:
#ifdef CIPHER_INT128_OK if ((align & 0xf) == 0) { // 对齐加载128位数据,单指令异或,对齐存储结果 __m128i src_vec = _mm_load_si128(srcp.u128_simd); __m128i key_vec = _mm_load_si128(bufp.u128_simd); __m128i res_vec = _mm_xor_si128(src_vec, key_vec); _mm_store_si128(destp.u128_simd, res_vec); } else #endif
优化后,原来的6条核心运算指令会缩减为3条向量指令,指令吞吐量提升一倍。
更高阶的优化方向
如果你的运行环境支持AVX2、AVX512指令集,可以进一步扩展到256位、512位向量运算,一次处理2/4个16字节AES块,性能提升会更明显:
- AVX2对应
_mm256_load_si256、_mm256_xor_si256等API,一次处理32字节数据 - AVX512对应
_mm512_load_si512、_mm512_xor_si512等API,一次处理64字节数据
如果需要兼容ARM架构,可以用NEON指令集的等价API实现同样的向量化逻辑,用编译宏做跨平台适配即可。
额外优化提示
你当前的循环一次仅处理1个16字节块,可以修改为一次处理4/8个块,减少循环分支的开销,同时提升CPU缓存的利用效率,进一步降低内存访问延迟带来的性能损耗。你观测到的XOR指令占比高,本质上大概率是内存访问瓶颈导致的,批量处理可以很好地缓解这个问题。
内容的提问来源于stack exchange,提问作者Chris Rapier

