并行/组合多个 64 位值的按位排列

Posted

技术标签:

【中文标题】并行/组合多个 64 位值的按位排列【英文标题】:Bitwise permutation of multiple 64bit values in parallel / combined 【发布时间】:2011-08-20 02:24:57 【问题描述】:

这个问题不是关于“我如何按位排列”我们现在如何做到这一点,我们正在寻找一种更快的方法,使用更少的 cpu 指令,灵感来自 DES 中 sbox 的位片实现强>

为了加快某些密码代码的速度,我们希望减少排列调用的数量。主要的密码函数基于查找数组进行多次按位排列。由于置换操作只是位移,

我们的基本思想是获取多个需要相同排列的输入值,然后并行移动它们。例如,如果输入位 1 必须移动到输出位 6。

有没有办法做到这一点?我们目前没有示例代码,因为完全不知道如何以高效的方式完成此操作。

我们平台上的最大值大小是128bit,最长输入值是64bit。因此代码必须更快,然后做整个排列128次。

编辑

这是一个简单的 8 位排列示例

+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <= Bits
+---+---+---+---+---+---+---+---+
+---+---+---+---+---+---+---+---+
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <= Input
+---+---+---+---+---+---+---+---+
| 3 | 8 | 6 | 2 | 5 | 1 | 4 | 7 | <= Output
+---+---+---+---+---+---+---+---+

密码使用多个输入键。这是一个分组密码,因此必须将相同的模式应用于输入的所有 64 位块。

由于每个输入块的排列相同,我们希望在一个步骤中处理多个输入块/以组合多个输入序列的操作。不是每次调用移动 128 次 1 位,而是一次移动 1 次 128 位。

EDIT2

我们不能使用线程,因为我们必须在没有线程支持的嵌入式系统上运行代码。因此我们也无法访问外部库,我们必须保持纯 C。

解决方案

在测试并使用给定的答案后,我们按照以下方式完成了:

我们将 128 个 64 位值的单个位放入 uint128_t[64]* 数组中。 对于置换,我们只需复制指针 完成所有操作后,我们恢复第一个操作并返回 128 个置换值

是的,就是这么简单。我们在项目早期就以这种方式进行了测试,但速度太慢了。看来我们在测试代码中有错误。

谢谢大家的提示和耐心。

【问题讨论】:

你能举出更多例子吗? “要获取多个输入值,需要相同的排列,然后并行移动它们”还不是很清楚。 您说您使用的是嵌入式平台 - 哪个?您的 CPU 的能力显然将是一个重要因素。 只针对permutation阶段,nemo的bit-slicing解决方案似乎并没有更快(如果我理解正确的话,ideone.com/OYORo的Version B应该会给你bit-slicing的方法) .但我认为他说得对:“在位片表示上实现所有阶段”。在我看来,这应该是一个优势。 @Nick 有一个保密协议,阻止我说太多。但我可以告诉你,我们的系统原生支持orandxornandnor,寻址模式从 8 位到 128 位,以及 256 个 128 位寄存器。 @Christian 感谢比较运行时的工作。我们将再次尝试使用位切片。我们已经尝试过了,它比你的例子慢得多。所以,看看代码是否有任何问题。 【参考方案1】:

您可以使用八个将字节映射到 64 位字的查找表来加快 Stan 的逐位代码。要处理来自输入的 64 位字,请将其分成 8 个字节并从不同的查找表中查找每个字节,然后对结果进行 OR。在我的计算机上,后者比 32 位排列的逐位方法快 10 倍。显然,如果您的嵌入式系统几乎没有缓存,那么 32 kB 16 kB 的查找表可能是个问题。如果一次处理 4 位,则只需要 16 个 16*8=128 字节的查找表,即 2 kB 的查找表。

编辑:内部循环可能看起来像这样:

void permute(uint64_t* input, uint64_t* output, size_t n, uint64_t map[8][256])

    for (size_t i = 0; i < n; ++i) 
        uint8_t* p = (uint8_t*)(input+i);
        output[i] = map[0][p[0]] | map[1][p[1]] | map[2][p[2]] | map[3][p[3]]
            | map[4][p[4]] | map[5][p[5]] | map[6][p[6]] | map[7][p[7]];
    

【讨论】:

因为我必须将位从任何位置移动到任何位置,我无法将输入和输出拆分为 8 位。而且我们必须一点一点地使用,因为它是一个 BITWISE 密码 ... 请注意,这个想法是将一个字节映射到一个 64 位字中,只是为了允许在 64 位字中的任何位置移动位。只要您不需要将位移动到另一个 64 位字中,它就可以正常工作。 那么,你应该举个例子,因为我现在无法想象 我添加了一个内部循环的示例实现。设置查找表作为练习留给读者... 我想这取决于处理器。我将代码添加到 Christian Ammer 的基准测试(作为版本 C),结果如下:版本 A 时间 = 0.675184;版本 B 时间 = 1.3193;版本 C 时间 = 0.016659【参考方案2】:

我想您可能正在寻找bit-slicing implementation。这就是最快的 DES 破解工具的工作原理。 (或者无论如何,它是在 SSE 指令存在之前。)

我们的想法是以“逐位”方式编写函数,将每个输出位表示为输入位上的布尔表达式。由于每个输出位仅取决于输入位,因此任何函数都可以用这种方式表示,即使是加法、乘法或 S-box 查找等。

诀窍是使用单个寄存器的实际位来表示来自多个输入字的单个位

我将用一个简单的四位函数来说明。

假设,例如,您想采用以下形式的四位输入:

x3 x2 x1 x0

...对于每个输入,计算一个四位输出:

x2 x3 x2^x3 x1^x2

您想为八个输入执行此操作。 (对于四位来说,查找表是最快的。但这只是为了说明原理。)

假设您的八个输入是:

A = a3 a2 a1 a0
B = b3 b2 b1 b0
...
H = h3 h2 h1 h0

这里,a3 a2 a1 a0 代表A 输入的四位等。

首先,将所有八个输入编码为四个字节,其中每个字节保存八个输入中的每个输入的一个位:

X3 =  a3 b3 c3 d3 e3 f3 g3 h3
X2 =  a2 b2 c2 d2 e2 f2 g2 h2
X1 =  a1 b1 c1 d1 e1 f1 g1 h1
X0 =  a0 b0 c0 d0 e0 f0 g0 h0

这里,a3 b3 c3 ... h3X3 的八位。它由所有八个输入的高位组成。 X2 是所有八个输入的下一位。以此类推。

现在要并行计算八次函数,您只需:

Y3 = X2;
Y2 = X3;
Y1 = X2 ^ X3;
Y0 = X1 ^ X2;

现在 Y3 保存所有八个输出的高位,Y2 保存所有八个输出的下一个位,依此类推。我们只是使用四个机器指令在八个不同的输入上计算了这个函数!

更好的是,如果我们的 CPU 是 32 位(或 64 位),我们可以在 32 个(或 64 个)输入上计算此函数,仍然只使用 4 条指令。

当然,对“位片”表示的输入进行编码和对输出进行解码需要一些时间。但是对于正确的函数类型,这种方法提供了巨大的位级并行性,从而大大提高了速度。

基本假设是您有许多输入(如 32 或 64),您希望在这些输入上计算相同的函数,并且该函数既不太难也不太容易表示为一堆布尔运算。 (太难使原始计算变慢;太容易使时间由位片编码/解码本身支配。)特别是对于密码学,其中(a)数据必须经过许多“轮”处理,(b ) 该算法通常已经根据位进行了修改,并且 (c) 例如,您正在对同一数据尝试许多密钥......它通常工作得很好。

【讨论】:

感谢您对位切片的精彩描述。但我不明白我们如何用这个实现来做基于矩阵的排列。当然,我们已经在代码的某些部分使用了 bitslice,但我不知道这如何帮助我们进行排列,就像我解释的那样...... @Thomas:但是排列很简单。在我的示例中,每个输入的 x3 和 x2 位被交换(置换)以产生相应的输出......再说一次,无论您的函数是什么(甚至是查找表),都存在 some用对输入位的布尔运算来表示它。 (布尔逻辑是通用的。) 请解释一下:我如何移动 128 个 64 位输入这种样式:输入位 1(对于 128 个中的每一个)必须在 128 个不同的输出中输出位 58。例如,第 58 位到第 9 位。你提到了DES。以任何排列(不是 sbox)为例 ;) 您的寄存器是 64 位的吗?然后让我们改用 64 个 64 位输入。首先从 all 64 个输入中取出位 0,并将它们放入一个 64 位字 X0 中。然后从 all 64 个输入中取出第 1 位,并将它们放入一个 64 位字 X1 中。依此类推,直到 X63。 (是的,这部分很慢,但你只做一次。)现在如果输出位 9 需要等于输入位 58,只需设置“Y9 = X58”。等等。最后,Y0 保存 all 64 个输出的第 0 位。 Y1 保存 all 64 个输出的第 1 位。等等。只需 64 条指令,您就可以对 64 个输入进行任意 64 位排列...使用位片编码 假设您通过多个阶段获取输入(对于加密,您肯定是?),您可以在开始时进行 once 转换,实现 all 位片表示上的阶段,然后在最后变换回 一次。这就是 DES 破解器的工作原理…… DES 有 16 个“回合”。他们实现了所有轮次以直接在位片编码形式上工作。位片实现的并行性足以弥补转换输入和输出的成本。【参考方案3】:

似乎很难在一次调用中完成排列。您的问题的一种特殊情况,即反转整数中的位,需要多个“调用”(调用是什么意思?)。有关此示例的信息,请参阅Bit Twiddling Hacks by Sean。

如果你的映射模式不复杂,也许你可以找到一种快速计算答案的方法:)但是,我不知道你是否喜欢这种直接的方式:

#include <stdio.h>

unsigned char mask[8];

//map bit to position
//0 -> 2
//1 -> 7
//2 -> 5
//...
//7 -> 6
unsigned char map[8] = 
    2,7,5,1,4,0,3,6
;


int main()

    int i;

    //input:
    //--------------------
    //bit 7 6 5 4 3 2 1 0
    //--------------------
    //val 0 0 1 0 0 1 1 0
    //--------------------
    unsigned char input = 0x26;

    //so the output should be 0xA1:
    //    1 0 1 0 0 0 0 1
    unsigned char output;

    for(i=0; i<8; i++) //initialize mask once
        mask[i] = 1<<i;
    

    //do permutation
    output = 0;
    for(i=0; i<8; i++)
        output |= (input&mask[i])?mask[map[i]]:0;
    

    printf("output=%x\n", output);
    return 0;

【讨论】:

我们已经这样做了。如果你必须使用 100 MB 的输入,那就太慢了 你的例子是一个函数调用。我们想要合并这些操作,因为位移的成本很高。【参考方案4】:

您最好的选择是研究某种类型的线程方案......您可以使用消息传递系统,将每个块发送到一组固定的工作线程,或者您可以设置一个非-锁定以“同步”方式执行多个班次的单个生产者/消费者队列。我说“同步”是因为通用 CPU 上的管道不会像在固定功能设备上那样是真正的同步管道操作,但基本上对于给定的“时间片”,每个线程都在工作同时处理多阶段问题的一个阶段,您可以将源数据“流式传输”进和流出管道。

【讨论】:

好吧,那么你不会得到任何真正的并行性......没有线程意味着你只会在单核上执行你的代码,而单核不能同时做两件事. 对不起,我有点不清楚,我想合并操作。 bitslicing 答案显示了 64 位 CPU 如何并行处理 64 位。所有并行的诀窍在于你在哪里找到它(线程,位并行)以及你如何利用它。

以上是关于并行/组合多个 64 位值的按位排列的主要内容,如果未能解决你的问题,请参考以下文章

如何有效地实现任意序列的按位循环?

python-opencv-图像的按位运算

如何并行删除多个表

简单的按位除二

相同值的并行写入

并行计算cuda笔记