在 x64 系统上递增一串长字的最快汇编代码
Fastest assembly code to increment a string of longwords on x64 system
很久以前,我对 Z80 和 68000 汇编程序进行了一些业余爱好编程,所以我了解基础知识,但我是 x86/x64 汇编程序的新手。我正在尝试找到 ** 最快 ** 代码来执行以下操作(需要最佳速度的关键部分仅是步骤 2-4)。
我根本不需要步骤 1 或 5 的帮助,它们仅供参考。 我不是在寻找完整的代码编写,但希望得到任何有关此平台的最佳指令和算法的提示。有很多方法可以编写这样的例程,但通常最明显的方法是不是最优的。如果有人说 "try using the XYZ instruction" 之类的话,我会很好。此外,正如我在下面提到的,在汇编程序中使用数组可能不是最快的方法,因此关于如何优化数据结构以提高速度的任何建议也是我正在寻找的答案的一部分。 (x64 汇编程序甚至可以处理带索引的 4GB 数组吗?)
- 第 1 步。从文件中读取 "string" 个长字元素。每个元素都包含外部提供的数据,可以将其视为带符号的 31 位数字。
该文件通常很小(小于 100K),但 有时 可能有 1GB 的元素那么长。 不需要将所有元素同时读入内存,只要单个元素直接accessed/modified即可。本能地,这听起来像使用 4GB 数组是最快的,但我是 x64 汇编程序的新手,不确定数组的 overhead 是否会帮助或损害速度。
步骤 2. 将元素加 1。
第三步,检查符号标志(看增量是否设置高位)。
- 如果设置,则分支到将修改元素的例程,然后继续执行步骤 4..
- 如果没有设置则跳到第 5 步(退出)。
子程序所花费的时间不在本题范围之内,暂时可以直接使用return指令。 然而,子例程需要知道元素的索引。
第 4 步。移至下一个元素并重复第 2 步。
步骤 5. 关闭文件,保存所有修改的数据。
还有两个相关问题:
由于元素是 32 位,代码 运行 在 32 位系统上会更快吗?
如果第 2 步是增量而不是 1,代码会有何不同?
对 "TOO BROAD" 关闭标志的响应:
这个问题 "Too Broad" 如何 准确地 符合 SO Help Page 社区指南顶部的所有四个主题描述:
- 一个具体的编程问题——(如何优化一种特殊的数组处理)
- 一个软件算法 --(前面提到的数组算法,具体就是上面的2-4步)
- 程序员常用的软件工具 -- (x64汇编器是程序员很常用的)
- 一个软件开发独有的实用的、可回答的问题——(因为几个 answers/suggestions 是由@Jester、@PeterCordes 和@Ped7g 提供的,我会说这是不言而喻的)
请参阅 x86 标签 wiki,了解有关很多内容的信息。
为什么要在回信之前完成所有 I/O?您未指定的子例程是否需要随机访问任意元素?如果是这样,mmap(2)
你的整个文件。 (假设 POSIX 系统调用)。
如果不是,read(2)
放入一个大约 128kB 的缓冲区。 (小于二级缓存)。对其进行处理,然后 pwrite(2)
将其返回到您读取它的位置。 (或 lseek(2)
/write(2)
)。
您的子例程是否可能中止整个过程,导致文件没有修改?
您可以使用 SSE2 进行递增:使用 PADDD
/ MOVMSKPS
并行执行四个 32 位(双字)加法,然后提取符号位。在掩码结果上使用 test
查看是否有任何元素设置了符号位。如果是这样,请为这些元素调用子例程。
bsf
将找到第一个设置位。有 BMI1 或 BMI2 指令清除最低设置位 IIRC。您可以使用它来遍历掩码中的设置位。
或者,如果您发现向量中的任何元素都设置了符号位,则可以跳过向量存储回到数组中,而是使用标量代码再次遍历这些元素。这样做的好处是在调用子程序时数组中的相邻元素处于 "correct" 状态。
例如
;; set up constants
pcmpeqw xmm1, xmm1
psrld xmm1, 31 ; xmm1 = [ 1 1 1 1]
; rsi = start, rdi = one-past-the-end
; or maybe prefer keeping these in regs the subroutine won't clobber
.vectorloop:
movdqa xmm0, [rsi]
paddd xmm0, xmm1
movmskps eax, xmm1 ; pmovmskb would give us the high bit of every byte. This is just every dword element
test eax, eax
jnz .at_least_one_sign_bit_set
movdqa [rsi], xmm0 ; vector store back, since no elements had sign bits set
.resume_vectorloop: ; scalar code jumps back here when done
add rsi, 16
cmp rsi, rdi
jb .vectorloop
jmp all_done
.at_least_one_sign_bit_set:
; Array isn't modified at this point.
inc dword [rsi] ;; or better, load / inc / jns, passing the pointer and index to the subroutine, so it doesn't have to load again after the read-modify-write inc.
jns ...
;; maybe add rsi, 4 here, depending on how we want want to call the subroutine.
inc dword [rsi+4]
jns ...
...
jmp .resume_vectorloop ;; or duplicate the tail and cmp/jb to .vectorloop
这假设您的缓冲区是对齐的并且大小是矢量宽度的倍数,因此您不必关心未对齐或标量清理。您控制缓冲区,所以这应该很容易。 (除了mmap的长度部分,可能。但这不是一个很难解决的问题。)
很久以前,我对 Z80 和 68000 汇编程序进行了一些业余爱好编程,所以我了解基础知识,但我是 x86/x64 汇编程序的新手。我正在尝试找到 ** 最快 ** 代码来执行以下操作(需要最佳速度的关键部分仅是步骤 2-4)。
我根本不需要步骤 1 或 5 的帮助,它们仅供参考。 我不是在寻找完整的代码编写,但希望得到任何有关此平台的最佳指令和算法的提示。有很多方法可以编写这样的例程,但通常最明显的方法是不是最优的。如果有人说 "try using the XYZ instruction" 之类的话,我会很好。此外,正如我在下面提到的,在汇编程序中使用数组可能不是最快的方法,因此关于如何优化数据结构以提高速度的任何建议也是我正在寻找的答案的一部分。 (x64 汇编程序甚至可以处理带索引的 4GB 数组吗?)
- 第 1 步。从文件中读取 "string" 个长字元素。每个元素都包含外部提供的数据,可以将其视为带符号的 31 位数字。
该文件通常很小(小于 100K),但 有时 可能有 1GB 的元素那么长。 不需要将所有元素同时读入内存,只要单个元素直接accessed/modified即可。本能地,这听起来像使用 4GB 数组是最快的,但我是 x64 汇编程序的新手,不确定数组的 overhead 是否会帮助或损害速度。
步骤 2. 将元素加 1。
第三步,检查符号标志(看增量是否设置高位)。
- 如果设置,则分支到将修改元素的例程,然后继续执行步骤 4..
- 如果没有设置则跳到第 5 步(退出)。
子程序所花费的时间不在本题范围之内,暂时可以直接使用return指令。 然而,子例程需要知道元素的索引。
第 4 步。移至下一个元素并重复第 2 步。
步骤 5. 关闭文件,保存所有修改的数据。
还有两个相关问题:
由于元素是 32 位,代码 运行 在 32 位系统上会更快吗?
如果第 2 步是增量而不是 1,代码会有何不同?
对 "TOO BROAD" 关闭标志的响应:
这个问题 "Too Broad" 如何 准确地 符合 SO Help Page 社区指南顶部的所有四个主题描述:
- 一个具体的编程问题——(如何优化一种特殊的数组处理)
- 一个软件算法 --(前面提到的数组算法,具体就是上面的2-4步)
- 程序员常用的软件工具 -- (x64汇编器是程序员很常用的)
- 一个软件开发独有的实用的、可回答的问题——(因为几个 answers/suggestions 是由@Jester、@PeterCordes 和@Ped7g 提供的,我会说这是不言而喻的)
请参阅 x86 标签 wiki,了解有关很多内容的信息。
为什么要在回信之前完成所有 I/O?您未指定的子例程是否需要随机访问任意元素?如果是这样,mmap(2)
你的整个文件。 (假设 POSIX 系统调用)。
如果不是,read(2)
放入一个大约 128kB 的缓冲区。 (小于二级缓存)。对其进行处理,然后 pwrite(2)
将其返回到您读取它的位置。 (或 lseek(2)
/write(2)
)。
您的子例程是否可能中止整个过程,导致文件没有修改?
您可以使用 SSE2 进行递增:使用 PADDD
/ MOVMSKPS
并行执行四个 32 位(双字)加法,然后提取符号位。在掩码结果上使用 test
查看是否有任何元素设置了符号位。如果是这样,请为这些元素调用子例程。
bsf
将找到第一个设置位。有 BMI1 或 BMI2 指令清除最低设置位 IIRC。您可以使用它来遍历掩码中的设置位。
或者,如果您发现向量中的任何元素都设置了符号位,则可以跳过向量存储回到数组中,而是使用标量代码再次遍历这些元素。这样做的好处是在调用子程序时数组中的相邻元素处于 "correct" 状态。
例如
;; set up constants
pcmpeqw xmm1, xmm1
psrld xmm1, 31 ; xmm1 = [ 1 1 1 1]
; rsi = start, rdi = one-past-the-end
; or maybe prefer keeping these in regs the subroutine won't clobber
.vectorloop:
movdqa xmm0, [rsi]
paddd xmm0, xmm1
movmskps eax, xmm1 ; pmovmskb would give us the high bit of every byte. This is just every dword element
test eax, eax
jnz .at_least_one_sign_bit_set
movdqa [rsi], xmm0 ; vector store back, since no elements had sign bits set
.resume_vectorloop: ; scalar code jumps back here when done
add rsi, 16
cmp rsi, rdi
jb .vectorloop
jmp all_done
.at_least_one_sign_bit_set:
; Array isn't modified at this point.
inc dword [rsi] ;; or better, load / inc / jns, passing the pointer and index to the subroutine, so it doesn't have to load again after the read-modify-write inc.
jns ...
;; maybe add rsi, 4 here, depending on how we want want to call the subroutine.
inc dword [rsi+4]
jns ...
...
jmp .resume_vectorloop ;; or duplicate the tail and cmp/jb to .vectorloop
这假设您的缓冲区是对齐的并且大小是矢量宽度的倍数,因此您不必关心未对齐或标量清理。您控制缓冲区,所以这应该很容易。 (除了mmap的长度部分,可能。但这不是一个很难解决的问题。)