我对此绝对感到震惊。我试图向自己展示C ++比现代PHP快多少。我在两者中运行了一个简单的CSV解析程序,它们具有相同的输出。 CSV文件为40,194,684,解析为1,537,194行。
编辑:这引发了比我预期更多的对话,这是两个程序均在其上运行的计算机的硬件统计信息,但实际上它是在nutanix服务器上运行的VM :
CPU:英特尔®至强®银4215R CPU @ 3.20GHz
内存:16GB
PHP代码(运行时42.750 s):
<?php
$i_fp = fopen("inFile.csv","r");
$o_fp = fopen("outFile.csv","w");

while(!feof($i_fp))
{
    $line = fgets($i_fp);
    $split = explode(';',$line);
    if($split[3] == 'E' || $split[3] == 'T')
    {
        fwrite($o_fp,join(',',[ $split[0], $split[1], $split[3], $split[4], $split[5], $split[6],
                                $split[10], $split[9],$split[11],$split[7],$split[32]])."\n");
    }
}
fclose($i_fp);
fclose($o_fp);

C ++代码(运行时3 m 59.074s)(使用g++ parse.cpp -o parse -O2 -std=c++1y编译)我如何使用stringvector以及是否需要在每个循环中重复调整它们的大小),但是我不确定这可能是什么。如果有人可以提供帮助,请向我们阐明。
编辑:不幸的是,我不能将输入文件作为敏感的内部文件提供。
感谢大家对这个内容以及所提供的所有建议非常感兴趣。我最近工作非常忙碌,无法再次访问,但希望尽快进行。

评论

编译和链接时是否使用-O3开关?

从标题中笑了起来。 :-)您可以共享输入文件吗?

“我试图向自己证明c ++比现代PHP快多少。” -您展示了一些经常被忽略的东西,即语言没有性能,用这些语言编写的程序在特定的编译和运行时配置中具有性能。请注意,PHP是一种编译语言,并且即使在使用JIT引擎的PHP 8之前,OpCache扩展程序也可以优化该编译后的代码,因此,如果您以较高的优化级别进行编译,请确保配置OpCache以进行公平的PHP比较。

关于“ PHP解析器”:您不是要解析的PHP程序(不是PHP解析器本身)吗?

这是一个很棒的线程。我认为它表明了一些经常被忽视的东西。一名普通的合格程序员将在2020年使用脚本语言(PHP,Python等)获得良好的性能。要使用传统语言获得更好的性能,就需要并非所有人都具备的高级知识。

#1 楼

始终配置文件优化的代码。 />使用-O3优化:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html


使用探查器:https://github.com/KDAB / hotspot

https://en.wikipedia.org/wiki/List_of_performance_analysis_tools




减少重复
string inPath = argv[1];
string outPath = argv[2];

std::ifstream inFile;
std::ofstream outFile;

inFile.open(inPath.c_str());
outFile.open(outPath.c_str());

to
std::ifstream inFile(argv[1]);
std::ofstream outFile(argv[2]);



尽可能避免字符串操作和分配。如果仅读取字符串,则优先使用std::string_view。只是为了使代码更美观
  outFile << makeCSVLine(split);

,您要付出巨大的时间代价。或使用较旧的旧版(如下所述)string makeCSVLine(vector<string> splitLine)(如果发现更快)。如果没有明显的时间增加,请遵循准则并使用fmt +流。答案,但在单独的代码块中。
另请参见fmt的速度测试。源文件


{fmt} fprintf并通过引用将其填充到函数内部(返回类型为inline)。这使它独立于返回值优化。所有编译器都将以相同的方式处理它。
也可以考虑将vector<string> splitStr(string line, const char delimiter = ',')和/或vector用于向量。已对void进行了测试以提高性能。


使用带分隔符的.reserve() + .emplace_back()。如果您怀疑这会浪费时间,请进行配置。不要猜测性能结果,不要对其进行测量。
 fmt::print(<FILE*>,
           "{},{},{},{},{},{},{},{},{},{},{}\n",
           vec[0],
           vec[1],
           vec[3],
           vec[4],
           vec[5],
           vec[6],
           vec[10],
           vec[9],
           vec[11],
           vec[7],
           vec[32]);



如果阅读器或写入器是最大的浪费时间,则避免reserve()stringstream在我的测试中速度提高了40%,灵活性没有损失(我用它来编写getlinefstreams s,它可能会有所不同(编辑:是的,它的变化很大,与流的其他好处相比,收益微不足道fprintf))和int s。)。
评论Stream IO和printf系列IO一样快,取自Herb Sutter和Bjarne Stroustrup: )指出,与iostream相比,printf()系列具有两个优点:格式化的灵活性和性能。必须权衡iostream的优势:可处理用户定义类型的可扩展性,可抵御安全性违规,隐式内存管理和区域设置处理的灵活性。
如果需要I / O性能,几乎总是可以做得比printf好()。

增强我的技能。

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#Rio-streams



在当前代码中,读取速度(float)通过拆分字符串和写入速度来限制。换句话说,只要文件编写器未完成工作,就无法读取更多行。您没有在这里充分利用磁盘的读取速度。
考虑将它们分开,以便所有读取一次完成并将数据存储在内存中并立即写出。
如果想要将峰值内存使用量降至最低,请使用线程并将读取器与(拆分器和写入器)分开在异步线程中。制作CSV文件的编写者代码,该代码源自Edward的一致性解答:https://ideone.com/gkmUUN
请注意,由于波动可能会影响3到5秒钟,因此应将足够接近的时序视为相同。
具有行长和字长知识的Matthew代码:2.6s
截至版本1的Matthew代码:2.7s
带有char数组存储和fmt的Edward算法:https://ideone.com/Kfkp90。这取决于以下知识:传入的CSV的最大行长为300个字符,最大字长为20个字符。 2.8s。
版本7之前的Martin代码:2.8s
对于易受错误影响的代码,以及处理未知长度的字符串:https://ideone.com/siFp3A。这是使用string的Edward算法。 4.1s。
getline(),其中parser.php从版本5开始为Edward的代码:4.4s
从版本1起为爱德华的代码:4.75s从版本为{fmt}的爱德华的代码https:// ideone。 com / 0Lmr5P:4.8
具有基本优化的OP代码,以及{fmt} https://ideone.com/5ivw9R:5.6s
问题中发布了OP的C ++代码:6.2s
OP的C ++代码带有gdate +%s.%3N && php parser.php && gdate +%s.%3N:6.4s
带有fprintf:45s
的OP的C ++代码

评论


\ $ \ begingroup \ $
评论不用于扩展讨论;此对话已移至聊天。
\ $ \ endgroup \ $
– Mathieu Guindon♦
20年8月1日在19:08

\ $ \ begingroup \ $
“程序在最该死的地方花费了惊人的时间”-匿名
\ $ \ endgroup \ $
–鲍勃·贾维斯(Bob Jarvis)-恢复莫妮卡
20年8月1日在21:11

#2 楼

概述
Akki在审查中做得很好。
我要强调的一些事情:

您通过值而不是使用const引用传递信息。

而不是构建输出字符串。有一个格式化程序对象,该对象知道如何转换流对象(更像是C ++)。
vector<string> splitStr(string const& line, const char delimiter = ',')
                               ^^^^^^
                               otherwise you are copying the line.

string makeCSVLine(vector<string> const& splitLine)
                                  ^^^^^^
                                  Copying a vector of strings that has to hurt.

现在MyFormat是只保留对splitLine的引用的对象。
 std::cout << MyFormat(splitLine);

但随后您编写了知道如何流式传输对象的输出格式化程序: C ++?

在优化战中出现了某些问题。使用string_view绝对有助于提高性能(对此并不感到意外)。
但最好的事情是简单地更新接口以使用string_view并重新编译,而无需更改其余代码。 >这应该工作
 struct MyFormat
 {
      std::vector<std::string> const&   data;
      MyFormat(std::vector<std::string> const& data) :data(data) {}
 };

,然后主体变得非常简单:
 std::ostream& operator<<(std::ostream& str, MyFormat const& value)
 {
      return str << value.data[0] << ","
                 << value.data[22] << "\n";
 }


评论


\ $ \ begingroup \ $
非常优雅!一个小细节是输入文件实际上在输入文件中使用分号作为分隔符。另一个是,如果读取的短行少于32个字段,最好进行错误检查。
\ $ \ endgroup \ $
–爱德华
20年7月30日在18:32

\ $ \ begingroup \ $
添加了CVS Range对象。
\ $ \ endgroup \ $
–马丁·约克
20年7月30日在19:22

\ $ \ begingroup \ $
更新为在行上使用string_view(这消除了制作每个元素的副本的需要)。我只存储行并跟踪每个元素的结束位置,从而允许我返回元素的string_view。
\ $ \ endgroup \ $
–马丁·约克
20年7月31日在18:00

#3 楼

您可以做很多事情来改进代码。
在可行的地方使用const引用
通过将它们作为const引用而不是按值传递,可以加快传递给函数的参数的速度。这样做会告诉编译器和其他代码读取器,不会更改传递的参数,并允许编译器进行其他优化。
使用reserve可以提高速度
,因为我们知道向量必须至少包含33个字段,使用reserve来预分配空间是很有意义的。
避免构造临时变量
而不是临时创建std::string来打印输出,替代方法是创建一个函数将它们直接输出到输出。
尽可能避免工作
虽然听起来可能是汤姆·索耶(Tom Sawyer)的生活理念,但这也是优化软件以提高性能的一个好主意。例如,由于代码正在寻找第四字段中的特定内容,因此如果在解析第四字段时尚未满足该条件,则没有理由继续解析该行。一种传递可能存在或不存在的值的方法是通过C ++ 17中引入的std::optional
结果
csv.cpp
#include <fstream>
#include <string>
#include <vector>
#include <sstream>
#include <optional>

constexpr std::size_t minfields{33};

std::optional<std::vector<std::string>> splitStr(const std::string& line, const char delimiter = ',')
{
    std::vector<std::string> splitLine;
    splitLine.reserve(minfields);
    std::istringstream ss(line);
    std::string buf;
    unsigned field{0};
    while (std::getline(ss, buf, delimiter)) {
        splitLine.push_back(buf);
        if (field == 3 && buf[0] != 'E' && buf[0] != 'T') {
            return std::nullopt;
        }
        ++field;
    }
    if (splitLine.size() < minfields)
        return std::nullopt;
    return splitLine;
}

std::ostream& writeLine(std::ostream& out, const std::vector<std::string>& splitLine)
{
    return out <<  
        splitLine.at(0) << ',' <<
        splitLine.at(1) << ',' <<
        splitLine.at(3) << ',' <<
        splitLine.at(4) << ',' <<
        splitLine.at(5) << ',' <<
        splitLine.at(6) << ',' <<
        splitLine.at(10) << ',' <<
        splitLine.at(9) << ',' <<
        splitLine.at(11) << ',' <<
        splitLine.at(7) << ',' <<
        splitLine.at(32) << '\n';
}

void copy_selective(std::istream& in, std::ostream& out) {
    std::string line;
    while(std::getline(in,line))
    {
        auto split = splitStr(line, ';');
        if (split) {
            writeLine(out, split.value());
        }
    }
}

int main(int argc, char* argv[])
{
    if(argc >= 3) {
        std::ifstream inFile(argv[1]);
        std::ofstream outFile(argv[2]);
        copy_selective(inFile, outFile);
    }
}
优化功能的GCC 10.1): > \ text {version}和\ text {time(s)}和\ text {相对于PHP} \\ 
\ hline
\ text {original}和2.161&1.17 \\
\ text {akki}&1.955&1.06 \\
\ text {akki w / writeLine}&1.898&1.03 \\
\ text {php}&1.851&1.00 \\
\ text {爱德华特/ printf}&1.483&0.80 \\
\ text {爱德华}&1.456&0.79 \\
\ text {Matthew}&0.737&0.40 \\
\ text {Martin York}&0.683&0.37
\ end {array}
$$
对于这些时间,标记为-O2的代码是https://ideone.com/5ivw9R,akki是相同的代码,但修改为使用上面显示的akki w/ writeLinewriteLine是此处显示的代码,修改为使用Edward w/ printf。在我的机器上的所有情况下,fprintf版本都比相应的fstream版本快。
输入文件
我创建了一个简单的文件,共有一百万行。如上所述,只有499980在第四字段中具有必需的“ E”或“ T”。所有行都是这四行之一的重复:
one;two;three;Efour;five;six;seven;eight;nine;ten;eleven;twelve;thirteen;fourteen;fifteen;sixteen;seventeen;eighteen;nineteen;twenty;twenty-one;twenty-two;twenty-three;twenty-four;twenty-five;twenty-six;twenty-seven;twenty-eight;twenty-nine;thirty;thirty-one;thirty-two;thirty-three;thirty-four
one;two;three;Tfour;five;six;seven;eight;nine;ten;eleven;twelve;thirteen;fourteen;fifteen;sixteen;seventeen;eighteen;nineteen;twenty;twenty-one;twenty-two;twenty-three;twenty-four;twenty-five;twenty-six;twenty-seven;twenty-eight;twenty-nine;thirty;thirty-one;thirty-two;thirty-three;thirty-four
one;two;three;four;five;six;seven;eight;nine;ten;eleven;twelve;thirteen;fourteen;fifteen;sixteen;seventeen;eighteen;nineteen;twenty;twenty-one;twenty-two;twenty-three;twenty-four;twenty-five;twenty-six;twenty-seven;twenty-eight;twenty-nine;thirty;thirty-one;thirty-two;thirty-three;thirty-four
one;two;three;Xfour;five;six;seven;eight;nine;ten;eleven;twelve;thirteen;fourteen;fifteen;sixteen;seventeen;eighteen;nineteen;twenty;twenty-one;twenty-two;twenty-three;twenty-four;twenty-five;twenty-six;twenty-seven;twenty-eight;twenty-nine;thirty;thirty-one;thirty-two;thirty-three;thirty-four

固定的PHP版本
因为我无法运行最初发布的PHP代码(它因错误而中止并生成了一个长度为0的文件) ,我打算对其进行最小的更改,以使其得以编译和运行。一个PHP专家(我不是一个)可能能够进一步改进它,但是它的性能相当不错,无需花费很多精力。 (以上使用的是PHP 7.4.8和Zend Engine v3.4.0。)
<?php
$i_fp = fopen("million.in","r");
$o_fp = fopen("sample.out","w") or die("Unable to open outfile");

while(!feof($i_fp))
{
    $line = fgets($i_fp);
    $split = explode(';',$line);
    if(count($split) > 33 && ($split[3][0] == 'E' || $split[3][0] == 'T')) {
        fwrite($o_fp,join(',',[ $split[0], $split[1], $split[3], $split[4], $split[5], $split[6],
                                $split[10], $split[9],$split[11],$split[7],$split[32]])."\n");
    }
}
fclose($i_fp);
fclose($o_fp);
?>


评论


\ $ \ begingroup \ $
评论不用于扩展讨论;此对话已移至聊天。
\ $ \ endgroup \ $
– Mathieu Guindon♦
20年8月1日在13:14

#4 楼

停止分配内存:

不要复制向量,而是通过const ref传递
当string_view可以使用时不要创建新的字符串
当使用string_view时不要创建新的字符串您可以重用旧的代码
不要用char *来创建字符串,而只是将其转换回char *(这个代码非常小,因为您只需要执行一次)
直接输出到避免在makeCSVLine中创建临时字符串

所有这些,这就是我想到的:
#include <fstream>
#include <string>
#include <string_view>
#include <vector>

using std::string;
using std::string_view;
using std::vector;

void splitStr(string_view line, const char delimiter, vector<string_view>& splitLine)
{
    splitLine.clear();
    for(;;) {
        std::size_t pos = line.find(delimiter);
        if (pos == string_view::npos) {
            splitLine.push_back(line);
            return;
        }

        splitLine.push_back(line.substr(0, pos));
        line = line.substr(pos+1, string_view::npos);
    }
}

template<typename T>
void makeCSVLine(T& out, const vector<string_view>& splitLine)
{
    out <<
        splitLine[0] << ',' <<
        splitLine[1] << ',' <<
        splitLine[3] << ',' <<
        splitLine[4] << ',' <<
        splitLine[5] << ',' <<
        splitLine[6] << ',' <<
        splitLine[10] << ',' <<
        splitLine[9] << ',' <<
        splitLine[11] << ',' <<
        splitLine[7] << ',' <<
        splitLine[32] << '\n';
}

int main(int argc, char* argv[])
{
    if(argc < 3)
    {
        exit(EXIT_SUCCESS);
    }

    const char* inPath = argv[1];
    const char* outPath = argv[2];

    std::ifstream inFile;
    std::ofstream outFile;

    inFile.open(inPath);
    outFile.open(outPath);

    vector<string_view> split;
    string line;
    while(std::getline(inFile, line))
    {
        splitStr(line, ';', split);
        if(split[3][0] == 'E' || split[3][0] == 'T')
        {
            makeCSVLine(outFile, split);
        }
    }
    inFile.close();
    outFile.close();
}


评论


\ $ \ begingroup \ $
非常好,唯一不使用std :: stringstream的答案!
\ $ \ endgroup \ $
– G. Sliepen
20年7月31日在12:57

\ $ \ begingroup \ $
太好了!到目前为止,这是我机器上最快的。
\ $ \ endgroup \ $
–爱德华
20年7月31日在15:00

\ $ \ begingroup \ $
通过添加错误检查,可以使其更耐用。在main的if中添加split.size()> 33子句可以解决一个问题(输入文件中的短行),而不会牺牲任何可衡量的性能。
\ $ \ endgroup \ $
–爱德华
20年7月31日在15:11

\ $ \ begingroup \ $
@ G.Sliepen更新了我的游戏,可以使用string_view进行娱乐。
\ $ \ endgroup \ $
–马丁·约克
20年7月31日在18:28

#5 楼

最初,我写了一个与PHP部分有关的答案,建议使用专用功能分别读写csv,fgetcsv()fputcsv(),但是我没有测试代码。感谢@akki指出了一些错误和分析结果,我了解到这些功能的运行速度大大降低,如本答案所述。看起来fgetcsv()比fread / explode慢40倍。但是,要解析具有字段定界符和转义符的适当的csv,则无论如何都必须使用适当的函数。
这是代码
/>在具有100万行的文件中

评论


\ $ \ begingroup \ $
几个问题:1它给出语法错误,因为它在while(..){{。。}之后有两个开括号。2它不向outFile写任何东西,它保持零字节。 3完成过程需要9秒钟(将其与我的答案中的计时进行比较。)//运行此快速脚本以创建文件ideone.com/gkmUUN并运行程序进行测试。我使用的命令(如果有的话)gdate +%s。%3N && php parser.php && gdate +%s。%3N
\ $ \ endgroup \ $
– aki
20年8月2日在8:28



\ $ \ begingroup \ $
老兄,我的版本要花很长时间才能完成。
\ $ \ endgroup \ $
–您的常识
20年8月2日在8:58

\ $ \ begingroup \ $
如果我没看错的话(这里是php noob),它将调用fgetcsv一百万次,fputcsv五万次。由于系统调用,即使在C ++中,这也将非常糟糕。 php可以缓冲文件内容吗?
\ $ \ endgroup \ $
– aki
20年8月2日在9:32



\ $ \ begingroup \ $
stackoverflow.com/questions/2749441/…显然,OP已经使用一种快速的方法来读取php中的文件。
\ $ \ endgroup \ $
– aki
20年8月2日在10:05



\ $ \ begingroup \ $
好吧,我们当然可以以牺牲内存为代价来进行缓冲,但是OP中的代码也不缓冲。 fgetcsv似乎非常慢,比fgets / explode慢40倍。 fputcsv更好,但仍然比join / fwrite慢几倍
\ $ \ endgroup \ $
–您的常识
20年8月2日在10:25



#6 楼

其他答案在分析代码方面做得很好,但是它们遗漏了最明显的地方。不要用C ++或C语言编写解析器。如果输入相当简单,则使用(f)lex;如果输入复杂,则使用flex + yacc / bison。或可能有其他一些为该工作设计的工具集,但这是最常见的。您的输入对于独立的Flex分析器来说足够简单。
https://en.wikipedia.org/wiki/Flex_(lexical_analyser_generator)
https://en.wikipedia.org/wiki/GNU_Bison

评论


\ $ \ begingroup \ $
我同意不(至少最初)不使用C ++编写解析器的观点。但是,在这种情况下,我们实际上并没有处理语法文件(因为这是一种成为文件语法文件的简单方法,它只是一种文件格式),这完全可以由C ++来完成。通过将其制成语法器并使用FLEX / Yacc,您将使所得代码更加复杂。
\ $ \ endgroup \ $
–马丁·约克
20年8月1日,0:03



\ $ \ begingroup \ $
@Martin York:不,如果您有语法(如编程语言),则需要使用YACC / Bison。 OP的输入似乎是简单的扫描仪可以处理的。 Flex处理样式,例如grep或其他正则表达式程序可以。该程序将简单得多,尽管当然存在学习过程。除非您当然是在谈论由flex生成的DFA程序,但通常情况下,您所关注的不只是查看编译器生成的机器代码。
\ $ \ endgroup \ $
–jamesqf
20年8月1日15:43

\ $ \ begingroup \ $
我使用Flex为此编写了一个扫描程序,它比我机器上的PHP版本慢。如果您写的更快,请将其作为新问题发布。
\ $ \ endgroup \ $
–爱德华
20年8月1日17:23

\ $ \ begingroup \ $
@@ MartinYork:拼写提醒:这是“语法”。
\ $ \ endgroup \ $
– Peter Cordes
20年8月1日在17:37

\ $ \ begingroup \ $
我已将Flex版本发布为新问题
\ $ \ endgroup \ $
–爱德华
20年8月1日在18:53