如何解决检查char数组中前导字符的最快方法是什么? 1编译器可以在调用该函数的地方内联该函数吗? 2不计算事物要比更有效地计算事物更快 3是n_zero很小还是更好? 4按位运算在这里有帮助吗?
我在代码中遇到了瓶颈,所以这个问题的主要问题是性能。
我有一个十六进制校验和,我想检查一个字符数组的前导零。这就是我正在做的:
bool starts_with (char* cksum_hex,int n_zero) {
bool flag {true};
for (int i=0; i<n_zero; ++i)
flag &= (cksum_hex[i]=='0');
return flag;
}
如果cksum_hex
有n_zero
前导零,则上述函数返回true。但是,对于我的应用程序,此功能非常昂贵(占总时间的60%)。换句话说,这是我的代码的瓶颈。所以我需要改善它。
我还检查了std::string::starts_with
(在C ++ 20中可用),但我发现性能没有差异:
// I have to convert cksum to string
std::string cksum_hex_s (cksum_hex);
cksum_hex_s.starts_with("000"); // checking for 3 leading zeros
有关更多信息,我使用的是g++ -O3 -std=c++2a
,我的gcc版本是9.3.1。
问题
- 检查char数组中前导字符的更快方法是什么?
- 使用
std::string::starts_with
可以更有效地做到这一点吗? - 按位运算在这里有帮助吗?
解决方法
如果您修改函数以使其尽早返回
dispatch
在获得大bool starts_with (char* cksum_hex,int n_zero) {
for (int i=0; i<n_zero; ++i)
{
if (cksum_hex[i] != '0') return false;
}
return true;
}
和n_zero
的情况下,速度会更快。否则,也许您可以尝试分配全局字符数组false
并使用'0'
:
std::memcmp
这里的问题是,您需要假设// make it as big as you need
constexpr char cmp_array[4] = {'0','0','0'};
bool starts_with (char* cksum_hex,int n_zero) {
return std::memcmp(cksum_hex,cmp_array,n_zero) == 0;
}
的最大可能值。
===编辑===
考虑到抱怨没有任何分析数据来证明建议的方法的合理性,请按以下步骤操作:
- Benchmark results比较早期回报实施与
n_zero
实施 - Benchmark results将
memcmp
实现与OP原始实现进行比较
使用的数据:
memcmp
const char* cs1 = "00000hsfhjshjshgj";
const char* cs2 = "20000hsfhjshjshgj";
const char* cs3 = "0000000000hsfhjshjshgj";
const char* cs4 = "0000100000hsfhjshjshgj";
在所有情况下都是最快的,但memcmp
具有提前返回暗示。
大概还拥有二进制校验和?,而不是先将其转换为ASCII文本,而是查看4*n
高位以直接检查{{1} }},而不是检查n
个字节是否等于0
。
例如如果您将哈希(或哈希的高8个字节)作为n
或'0'
,则将其右移以仅保留高uint64_t
个半字节。
我展示了一些示例,说明当两个输入都是运行时变量时,它们如何针对x86-64进行编译,但它们也可以很好地编译至其他ISA(如AArch64)。这段代码都是可移植的ISO C ++。
unsigned __int128
clang使用n
在x86-64上做得很好,以启用BMI1 / BMI2
bool starts_with (uint64_t cksum_high8,int n_zero)
{
int shift = 64 - n_zero * 4; // A hex digit represents a 4-bit nibble
return (cksum_high8 >> shift) == 0;
}
这甚至适用于-O3 -march=haswell
(shift = 0)来测试所有64位。 high_zero_nibbles(unsigned long,int):
shl esi,2
neg sil # x86 shifts wrap the count so 64 - c is the same as -c
shrx rax,rdi,rsi # BMI2 variable-count shifts save some uops.
test rax,rax
sete al
ret
无法测试所有位都失败;它会通过将n=16
移位> =其宽度来移位,从而遇到UB。 (在像x86这样的ISA封装了越界移位计数,用于其他移位计数的代码源将导致检查所有16位。只要在编译时看不到UB ...)希望您'仍然不打算用n_zero = 0
来称呼它。
其他选项:创建仅保留高uint64_t
位的掩码,如果准备晚于n_zero=0
,则可以缩短通过n*4
的关键路径。特别是如果cksum_high8
是内联后的编译时常量,则其速度可能与检查n_zero
一样快。 (例如x86-64 n_zero
。)
cksum_high8 == 0
或使用位扫描功能来计数前导零位并比较test reg,immediate
。不幸的是,ISO C ++ until C++20 <bit>
的bool high_zero_nibbles_v2 (uint64_t cksum_high8,int n_zero) {
int shift = 64 - n_zero * 4; // A hex digit represents a 4-bit nibble
uint64_t low4n_mask = (1ULL << shift) - 1;
return cksum_high8 & ~low4n_mask;
}
最终才可移植地公开了这种已经存在了数十年的通用CPU功能(例如386 >= 4*n
/ countl_zero
);在此之前,只能用作GNU C bsf
之类的编译器扩展。
如果您想知道有多少个且没有一个特定的临界阈值,那就太好了。
bsr
编译为(对于Haswell来说是c语):
__builtin_clz
所有这些指令在Intel和AMD上都很便宜,lzcnt和shl之间甚至还有一些指令级并行性。
See asm output for all 4 of these on the Godbolt compiler explorer。 Clang将1和2编译为相同的asm。使用bool high_zero_nibbles_lzcnt (uint64_t cksum_high8,int n_zero) {
// UB on cksum_high8 == 0. Use x86-64 BMI1 _lzcnt_u64 to avoid that,guaranteeing 64 on input=0
return __builtin_clzll(cksum_high8) > 4*n_zero;
}
#include <bit>
bool high_zero_nibbles_stdlzcnt (uint64_t cksum_high8,int n_zero) {
return std::countl_zero(cksum_high8) > 4*n_zero;
}
的两种方式都相同。否则,对于C ++ 20版本(不是UB),它需要不遗余力地处理input = 0的high_zero_nibbles_lzcnt(unsigned long,int):
lzcnt rax,rdi
shl esi,2
cmp esi,eax
setl al # FLAGS -> boolean integer return value
ret
极端情况。
要将这些扩展到更宽的哈希值,可以检查uint64_t的高位是否为全零,然后进行下一个uint64_t块。
使用SSE2与字符串-march=haswell
-> bsr
上的pcmpeqb
进行比较,可以找到前pmovmskb
位的位置,因此可以找到多少前导{ 1}}字符(如果有的话)包含在字符串表示中。因此,x86 SIMD可以非常有效地执行此操作,您可以通过内部函数从C ++使用它。
您可以使零缓冲区足够大,而不是与memcmp进行比较。
const char *zeroBuffer = "000000000000000000000000000000000000000000000000000";
if (memcmp(zeroBuffer,cksum_hex,n_zero) == 0) {
// ...
}
,
要检查的内容以使您的应用程序更快:
1。编译器可以在调用该函数的地方内联该函数吗?
要么在头文件中声明该函数为内联函数,要么将该定义放在使用它的编译单元中。
2。不计算事物要比更有效地计算事物更快
是否需要所有对该函数的调用?高成本通常是在高频环路内或在昂贵的算法中称为函数的标志。您通常可以通过优化外部算法来减少调用次数,从而减少函数花费的时间
3。是n_zero
很小还是更好?
对于通常较小的常数值,编译器非常擅长优化算法。如果编译器知道该常量,则很有可能将其完全删除。
4。按位运算在这里有帮助吗?
它肯定有作用,并且允许Clang(但据我所知,不能执行GCC)进行某些矢量化。向量化往往会更快,但并非总是如此,具体取决于您的硬件和处理的实际数据。
是否进行优化取决于n_zero
的大小。考虑到您正在处理校验和,它应该很小,所以听起来像是潜在的优化。
对于已知的n_zero
,使用按位运算允许编译器删除所有分支。我希望,尽管我没有测量,但速度会更快。
std::all_of
和std::string::starts_with
应该完全按照您的实现进行编译,除了它们将使用&&
代替&
。
在这个有趣的讨论中加上我的两分钱,尽管游戏有些迟了,但我认为您可以使用std::equal
,这是一种快速方法,但方法稍有不同,使用了最大数量的硬编码字符串零,而不是零。
这可以传递给要搜索的字符串的开头和结尾的函数指针,以及指向零的字符串,特别是指向begin
和end
的迭代器,end
指向期望的零个数中的一个过去的位置,它们将由std::equal
用作迭代器:
bool startsWith(const char* str,const char* end,const char* substr,const char* subend) {
return std::equal(str,end,substr,subend);
}
int main() {
const char* str = "000x1234567";
const char* substr = "0000000000000000000000000000";
std::cout << startsWith(&str[0],&str[3],&substr[0],&substr[3]);
}
使用@pptaszni's good answer中的测试用例和相同的测试条件:
const char* cs1 = "00000hsfhjshjshgj";
const char* cs2 = "20000hsfhjshjshgj";
const char* cs3 = "0000000000hsfhjshjshgj";
const char* cs4 = "0000100000hsfhjshjshgj";
比使用memcmp
慢,但仍然更快(除了零数少的错误结果外),并且比原始代码更一致。
除非n_zero
很高,否则我会同意其他人的观点,即您可能会误解分析器结果。但是无论如何:
-
是否可以将数据交换到磁盘?如果您的系统处于RAM压力之下,则在对它执行第一次操作时,数据可能会交换到磁盘上,并且需要重新加载回RAM中。 (假设此校验和检查是一段时间内对数据的首次访问。)
-
您有可能使用多个线程/进程来利用多核处理器。
-
也许您可以使用输入数据的统计信息/相关性,或问题的其他结构特征。
- 例如,如果您有很多数字(例如50),并且您知道后面的数字非零的可能性更高,则可以先检查最后一位。
- 如果几乎所有的校验和都应该匹配,则可以使用
[[likely]]
来向编译器提示这种情况。 (可能不会有所作为,但值得一试。)
使用https://docs.saltstack.com/en/latest/topics/grains/
return std::all_of(chsum_hex,chsum_hex + n_zero,[](char c){ return c == '0'; })
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。