isdigit() 在 c++ 中不应该更快吗?

Shouldn't isdigit() be faster in c++?

本文关键字:不应该 c++ isdigit      更新时间:2023-10-16

我在 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 [我选择的编译器] 中,isdigitis_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提出的基准测试陷入了几个陷阱:

  1. 使用常量字符 c = '8'; ...任何编译器都会明白它不会改变,并且可以缓存或跳过结果。
  2. 循环正在运行一个函数,但结果没有在任何地方使用 => 同样,编译器可以完全跳过循环。
  3. 它没有考虑 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::isdigitis_digit2 函数一样快
  • 在 MSVC 上,std::isdigitis_digit2 慢 9 倍(但这可能是由于区域设置(