常见的对称密钥算法中,S盒置换是核心环节之一,提高S盒置换的效率,会对整体密码运算效率产生巨大影响。说到这里,也许会有人质疑我这个提法,S盒置换不就是给定数组下标从字符数组取出一个字节替换对应字节嘛,这么简单的事情有什么好优化的?确实,如果是在8位CPU上进行S盒置换,没有比字节数组寻址更有效的实现了,但是在x64平台上,这个问题就不那么简单了。
首先贴最基础的SMS4算法中的S盒置换部分:(文件名:sbox.c)
#include <stdint.h>
static uint8_t s_sbox[256] = {
0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05,
0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99,
0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62,
0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6,
0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8,
0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35,
0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87,
0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e,
0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1,
0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3,
0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f,
0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51,
0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8,
0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0,
0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84,
0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48
};
uint32_t sbox(uint32_t src)
{
union ex {
uint32_t u32;
uint8_t u8[4];
} data;
data.u32 = src;
data.u8[0] = s_sbox[data.u8[0]];
data.u8[1] = s_sbox[data.u8[1]];
data.u8[2] = s_sbox[data.u8[2]];
data.u8[3] = s_sbox[data.u8[3]];
return(data.u32);
}
然后是性能测试代码:(文件名:test_sbox.c)
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
uint32_t sbox(uint32_t src);
int main(int argc, char *argv[])
{
uint32_t i, data;
data = 0x00010203;
for(i = 0; i < 100000000; i++) {
data = sbox(data);
}
printf("data = %08x\n", data);
exit(EXIT_SUCCESS);
}
用以下编译指令和参数编译性能测试程序:
gcc -Wall -O2 test_sbox.c sbox.c -o test_sbox
运行性能测试程序同事输出耗时数据:
[root@sxy-lenovo base]# time ./test_sbox
data = 9acd23e0
real 0m1.235s
user 0m1.236s
sys 0m0.000s
下面开始第一次优化,将文件sbox.c中函数sbox()里的union去掉,改成位运算实现,代码如下:
uint32_t sbox(uint32_t src)
{
uint32_t dst;
dst = s_sbox[src & 0xff] ^
(s_sbox[(src >> 8) & 0xff] << 8) ^
(s_sbox[(src >> 16) & 0xff] << 16) ^
(s_sbox[src >> 24] << 24);
return(dst);
}
编译后测速:
[root@sxy-lenovo step1]# time ./test_sbox
data = 9acd23e0
real 0m0.630s
user 0m0.629s
sys 0m0.000s
看起来不错,性能提高接近一倍,将两个版本的sbox()函数分别编译成汇编语言对比发现,两者在汇编代码上大同小异,优化后版本仅比优化前版本少了两条andl指令,其余指令大体相同,最终性能却相差一倍,有鉴于此,如果C代码中使用了union,深度优化时建议检查其汇编代码是否有冗余指令。
再进一步,将用于S盒差表的静态数组的数据类型从8位改成32位,也就是说,原先占用256字节的S盒静态数组,现在要占用256 x 4 = 1024字节,其它地方不变,文件sbox.c的代码变成下面这样:
#include <stdint.h>
static uint32_t s_sbox[256] = {
0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05,
0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99,
0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62,
0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6,
0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8,
0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35,
0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87,
0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e,
0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1,
0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3,
0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f,
0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51,
0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8,
0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0,
0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84,
0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48
};
uint64_t sbox(uint32_t src)
{
uint32_t dst;
dst = s_sbox[src & 0xff] ^
(s_sbox[(src >> 8) & 0xff] << 8) ^
(s_sbox[(src >> 16) & 0xff] << 16) ^
(s_sbox[src >> 24] << 24);
return(dst);
}
编译后测速,得到如下结果:
[root@sxy-lenovo step2]# time ./test_sbox
data = 9acd23e0
real 0m0.610s
user 0m0.608s
sys 0m0.002s
较之上一版本,提升了大约3%的性能,这点性能提升是哪里来的呢,我认为是数据对齐的影响,如果真是这样,那么修改S盒静态数组到64位位宽就会达到此优化的性能极限。
将S盒数组位宽转为64位后,内存占用为256 x 8 = 2048字节,执行测试程序结果如下:
[root@sxy-lenovo step3]# time ./test_sbox
data = 9acd23e0
real 0m0.570s
user 0m0.568s
sys 0m0.002s
从测试结果可以看出,尽管S盒数组占用内存从256字节增加到2048字节,运算时间节约了:
(0.63 - 0.57) / 0.63 = 9.5%
这相当于10%的性能提升,在追求极限性能的场合,内存对齐是不可忽视的因素。