isdigit() 在 c++ 中不应该更快吗?
Shouldn't isdigit() be faster in c++?
我在 c++ 中使用isdigit()
函数,但我发现它很慢,所以我实现了自己的is_digit()
,请参阅下面的代码:
#include<iostream>
#include<cctype>
#include<ctime>
using namespace std;
static inline bool is_digit(char c)
{
return c>='0'&&c<='9';
}
int main()
{
char c='8';
time_t t1=clock(),t2,t3;
for(int i=0;i<1e9;i++)
is_digit(c);
t2=clock();
for(int i=0;i<1e9;i++)
isdigit(c);
t3=clock();
cout<<"is_digit:"<<(t2-t1)/CLOCKS_PER_SEC<<"nisdigit:"<<(t3-t2)/CLOCKS_PER_SEC<<endl;
return 0;
}
运行后,is_digit()
只用了1秒(1161ms(,isdigit()
花了4秒(3674ms(,我知道isdigit
是通过位操作实现的,isdigit()
不应该比is_digit()
快吗?
更新1
我使用带有默认选项的MS VS2010,发布版本,如何使isdigit()
比VS中的is_digit()
更快?
更新2
谢谢大家。在VS中处于发布模式时,项目将针对速度默认值(-O2(进行优化。
全部处于发布模式。
VS2010:is_digit:1182毫秒数字:3724(毫秒(
VS2013:is_digit:0(毫秒(数字:3806(毫秒(
带有 g++(4.7.1( 和 -O3 的代码块:is_digit:1275(毫秒(数字:1331(毫秒(
所以这是结论:
is_digit()
在 VS 中比 isdigit()
快,但在 g++ 中比 isdigit()
慢。
g++ 中的isdigit()
比 VS 中的isdigit()
更快。
所以"VS 在性能上很糟糕"?
在 clang/llvm [我选择的编译器] 中,isdigit
和 is_digit
将变成完全相同的代码,因为它针对特定的库调用进行了优化,以将其转换为 ((unsigned)(c-48) < 10u)
。
return c>='0' && c <='9';
也通过优化(作为编译器执行的通用if x >= N && x <= M
-> x-N > (M-N)
转换(转换为c-48 > 10
。
因此,从理论上讲,两个循环都应该变成相同的代码(至少使用具有这种isdigit
优化的编译器 - 无论MSVC是否这样做,我不能说,因为源代码不向公众开放(。我知道 gcc 有类似的代码来优化库调用,但我的机器上目前没有 gcc 源代码,我懒得去查找它 [根据我的经验,无论如何,它会比 llvm 代码更难阅读]。
llvm 中的代码:
Value *LibCallSimplifier::optimizeIsDigit(CallInst *CI, IRBuilder<> &B) {
Function *Callee = CI->getCalledFunction();
FunctionType *FT = Callee->getFunctionType();
// We require integer(i32)
if (FT->getNumParams() != 1 || !FT->getReturnType()->isIntegerTy() ||
!FT->getParamType(0)->isIntegerTy(32))
return nullptr;
// isdigit(c) -> (c-'0') <u 10
Value *Op = CI->getArgOperand(0);
Op = B.CreateSub(Op, B.getInt32('0'), "isdigittmp");
Op = B.CreateICmpULT(Op, B.getInt32(10), "isdigit");
return B.CreateZExt(Op, CI->getType());
}
对于那些不熟悉LLVM代码的人:它首先检查函数调用是否具有正确数量的参数和参数类型。如果失败,它将返回 NULL 以指示"我无法优化此"。否则,它会构建操作链,使用无符号比较来执行if (c - '0' > 10)
,以应对"负"值[在无符号中是巨大的值]。
如果这样做会出错:
bool isdigit(int x)
{
return image_contains_finger(imagefiles[x]);
}
[但是,用你自己的版本替换库函数,做一些事情,通常会有有趣的效果!
通过以下方式更快地实现您的函数is_digit:
#define ISDIGIT(X) (((uint32_t)X - '0') < 10u)
保存一个比较的位置。我认为,这是 gcc 中的正常应用,但在 Visual Studio Microsoft我猜你有一个本地化版本的 isdigit(((因此需要很长时间来检查语言环境(。
看看这段代码(适用于 g++(和 -O3
#include<iostream>
#include<cctype>
#include<ctime>
#include <time.h>
#include <sys/time.h>
using namespace std;
static inline bool is_digit(char c)
{
return c>='0'&&c<='9';
}
int main()
{
char c='8';
struct timeval tvSt, tvEn;
time_t t1=clock(),t2,t3;
gettimeofday(&tvSt, 0);
for(int i=0;i<1e9;i++)
is_digit(c);
gettimeofday(&tvEn, 0);
cout << "is_digit:" << (tvEn.tv_sec - tvSt.tv_sec)*1000000 + (tvEn.tv_usec - tvSt.tv_usec) << " us"<< endl;
gettimeofday(&tvSt, 0);
for(int i=0;i<1e9;i++)
isdigit(c);
gettimeofday(&tvEn, 0);
cout << "isdigit:" << (tvEn.tv_sec - tvSt.tv_sec)*1000000 + (tvEn.tv_usec - tvSt.tv_usec) << " us"<< endl;
return 0;
}
结果:
is_digit:1610771 us
isdigit:1055976 us
因此,C++实施胜过您的实施。
通常,当您衡量性能时,用秒来衡量不是一个好主意。至少考虑微秒级别。
我不确定VS。请找出微秒级时钟并测量。
附言。请参阅 VS 优化 https://msdn.microsoft.com/en-us/library/19z1t1wy.aspx
@Doonyx提出的基准测试陷入了几个陷阱:
- 使用常量字符 c = '8'; ...任何编译器都会明白它不会改变,并且可以缓存或跳过结果。
- 循环正在运行一个函数,但结果没有在任何地方使用 => 同样,编译器可以完全跳过循环。
- 它没有考虑 CPU 性能增量,CPU 可能需要一些时间才能"唤醒",并且通常它的性能会随着时间的推移而变化。
=> 我对该基准进行了修改以解决所有 3 点。
// gcc main.cpp -O3 -std=c++20 -lstdc++ && ./a.out
#include <chrono>
#include <iomanip>
#include <iostream>
#include <map>
#include <vector>
// basic function
static inline bool is_digit(char c)
{
return c >= '0' && c <= '9';
}
// optimized function
constexpr bool is_digit2(int c)
{
return (uint32_t)(c - '0') < 10u;
}
constexpr int NUM_STEP = 8;
constexpr int TRIM = 2;
#define NOW_NS() std::chrono::high_resolution_clock::now().time_since_epoch().count()
int main()
{
int64_t sum;
std::map<std::string, std::vector<int64_t>> nameTimes;
std::map<std::string, int64_t> nameAvgs;
// convenience define to run the benchmark
#define RUN_BENCH(name, code)
do
{
const auto start = NOW_NS();
sum = 0;
for (int i = 0; i < 1000000000; ++i)
sum += code;
const auto name##Time = NOW_NS() - start;
nameTimes[#name].push_back(name##Time);
std::cout << step << " " << std::setw(11) << #name << ": "
<< std::setw(10) << name##Time << " ns sum=" << sum << std::endl;
}
while (0)
// 1) run the benchmark NUM_STEP times
// note that a null test is added to compute the overhead
for (int step = 0; step < NUM_STEP; ++step)
{
RUN_BENCH(_null, i & 15);
RUN_BENCH(is_digit, is_digit(i & 255));
RUN_BENCH(is_digit2, is_digit2(i & 255));
RUN_BENCH(std_isdigit, std::isdigit(i & 255));
}
// 2) remove the 25% slowest and 25% fastest runs for each benchmark (Interquartile range)
std::cout << "ncombining:n";
for (auto& [name, times] : nameTimes)
{
int64_t total = 0;
std::sort(times.begin(), times.end());
std::cout << std::setw(11) << name;
for (int i = 0; i < NUM_STEP; ++i)
{
std::cout << " " << i << ":" << times[i];
if (i >= TRIM && i < NUM_STEP - TRIM)
{
std::cout << "*";
total += times[i];
}
}
total /= (NUM_STEP - TRIM * 2);
std::cout << " => " << total << " nsn";
nameAvgs[name] = total;
}
// 3) show the results + results MINUS the overhead (null time)
std::cout << "nsummary:n";
for (auto& [name, time] : nameAvgs)
{
std::cout << std::setw(11) << name << ": " << std::setw(10) << time << " ns "
<< " time-null: " << std::setw(10) << time - nameAvgs["_null"] << " nsn";
}
return 0;
}
因此,每个基准测试都稍微复杂一些,并迫使编译器实际执行代码,它们按顺序运行,然后运行 8 次,以考虑 CPU 性能变化,然后丢弃最慢/最快的运行,并在最后的摘要中减去开销的时间,以了解函数的真实速度。
gcc 11.2.0 with -O0:
_null: 680327226 ns time-null: 0 ns
is_digit: 1368190759 ns time-null: 687863533 ns
is_digit2: 1223091465 ns time-null: 542764239 ns
std_isdigit: 733283544 ns time-null: 52956318 ns *
msvc 17.3.4 with -O0:
_null: 576647075 ns time-null: 0 ns
is_digit: 1348345625 ns time-null: 771698550 ns
is_digit2: 754253650 ns time-null: 177606575 ns *
std_isdigit: 1619403975 ns time-null: 1042756900 ns
gcc 11.2.0 with -O1:
_null: 217714988 ns time-null: 0 ns
is_digit: 459088203 ns time-null: 241373215 ns
is_digit2: 434988334 ns time-null: 217273346 ns *
std_isdigit: 435391905 ns time-null: 217676917 ns *
msvc 17.3.4 with -O1:
_null: 217425875 ns time-null: 0 ns
is_digit: 442688400 ns time-null: 225262525 ns *
is_digit2: 440954975 ns time-null: 223529100 ns *
std_isdigit: 1187352900 ns time-null: 969927025 ns
gcc 11.2.0 with -O2:
_null: 217411308 ns time-null: 0 ns
is_digit: 542259068 ns time-null: 324847760 ns
is_digit2: 434180245 ns time-null: 216768937 ns *
std_isdigit: 435705056 ns time-null: 218293748 ns *
msvc 17.3.4 with -O2:
_null: 209602025 ns time-null: 0 ns
is_digit: 441704325 ns time-null: 232102300 ns
is_digit2: 298747075 ns time-null: 89145050 ns *
std_isdigit: 1198361400 ns time-null: 988759375 ns
gcc 11.2.0 with -O3:
_null: 126789606 ns time-null: 0 ns
is_digit: 206127551 ns time-null: 79337945 ns
is_digit2: 175606336 ns time-null: 48816730 ns *
std_isdigit: 174991923 ns time-null: 48202317 ns *
msvc 17.3.4 with -Ox:
_null: 206283850 ns time-null: 0 ns
is_digit: 434584200 ns time-null: 228300350 ns
is_digit2: 312153225 ns time-null: 105869375 ns *
std_isdigit: 1176565150 ns time-null: 970281300 ns
结论:
- 在 GCC 上,
std::isdigit
与is_digit2
函数一样快 - 在 MSVC 上,
std::isdigit
比is_digit2
慢 9 倍(但这可能是由于区域设置(
- 条件断点在不应该触发时触发
- 你好。。。id_public变量不应该给出结果为 81 和 86 吗?为什么它为两个派生类占用不同的内存位置?
- 为什么我不应该把所有东西都放在标题中?
- 找不到 QRegularExpression 行为的任何解释。它有效,但不应该
- 在清除 istream 之前,我不应该需要取消获取它吗?
- c++ 为什么我不应该从不同的线程解锁互斥锁
- "typename"不应该只在模板函数或模板类中使用吗?
- 为不应该获得未定义行为的内容获取未定义的行为
- 两种情况下的输出不应该相同吗?
- 默认情况下,"std::shared_ptr"不应该使用"std::d efault_delete"吗?
- 错误代码 E0065 和 E0169 不应该有
- 矢量的值在不应该更改时更改
- 保证复制省略不应该适用吗?
- 不应该禁止访问私有类型吗?
- 使用匿名命名空间中的函数或另一个文件中的静态函数不应该出错吗?
- NRVO不应该保证局部命名变量和调用站点变量采用相同的地址吗?
- 类的私有成员在我的类实例化期间更改,即使他们不应该
- 为什么'system'不应该在 Windows 特定的应用程序中使用
- 正则表达式在我认为不应该在 c++ 中采用等号
- OpenGL - 深度缓冲区在渲染半透明立方体时剪切掉不应该剪切的面