【问题标题】:C++ Perfomance Per Compiler, 200 times slower than C#每个编译器的 C++ 性能,比 C# 慢 200 倍
【发布时间】:2019-10-23 22:14:54
【问题描述】:

我正在处理我在这个问题中讨论的一些性能问题:Super Slow C++ For Loop

我编写了一个简单的程序来解析二进制数据。我在 2 台计算机上进行了本地测试。

1. Dual 6 core 2.4GHz Xeon V3, 64GB RAM, NVMe SSD
2. Dual 4 core 3.5GHz Xeon V3, 64GB RAM, NVMe SSD

这里是一些代码(其余在 Wandbox https://wandbox.org/permlink/VIvardJNAMKzSbMf):

string HexRow="";
for (int i=b; i<HexLineLength+b;i++){
    HexRow+= incomingData[i];
}

std::vector<unsigned char> BufferedLine=HexToBytes(HexRow);
stopwatch<> sw;
for (int i = 0; 80 >= i; ++i)
{
    Byte ColumnBytes;
    for (auto it = columns["data"][i].begin(); it != columns["data"][i].end(); ++it)
    {
        try {
            if (it.key() == "Column") { ColumnBytes.Column = it.value().get<std::string>();}
            else if (it.key() == "DataType") { ColumnBytes.DataType = it.value().get<std::string>();}
            else if (it.key() == "StartingPosition") { ColumnBytes.StartingPosition = it.value().get<int>();}
            else if (it.key() == "ColumnWidth") { ColumnBytes.ColumnWidth = it.value().get<int>();}
        }
        catch (...) {}
    }

    char* locale = setlocale(LC_ALL, "UTF-8");
    std::vector<unsigned char> CurrentColumnBytes(ColumnBytes.ColumnWidth);
    int arraySize = CurrentColumnBytes.size();

    for (int C = ColumnBytes.StartingPosition; C < ColumnBytes.ColumnWidth + ColumnBytes.StartingPosition; ++C)
    {
        int Index = C - ColumnBytes.StartingPosition;
        CurrentColumnBytes[Index] = BufferedLine[C-1];
    }
}
std::cout << "Elapsed: " << duration_cast<double>(sw.elapsed()) << '\n';

PC 1

使用以下标志在 PC 1 上使用 Visual Studio 进行编译:

/O2 /JMC /permissive- /MP /GS /analyze- /W3 /Zc:wchar_t /ZI /Gm- /sdl /Zc:inline /fp:precise /D "_CRT_SECURE_NO_WARNINGS" /D "_MBCS" /errorReport:prompt /WX- /Zc:forScope /Gd /Oy- /MDd /std:c++17 /FC /Fa"Debug\" /EHsc /nologo /Fo"Debug\" /Fp"Debug\Project1.pch" /diagnostics:column

输出:

Elapsed: 0.0913771
Elapsed: 0.0419886
Elapsed: 0.042406

将 Clang 与以下内容一起使用:clang main.cpp -O3 输出:

Elapsed: 0.036262
Elapsed: 0.0174264
Elapsed: 0.0170038

使用这些开关 gcc main.cpp -lstdc++ -O3 从 MinGW gcc version 8.1.0 (i686-posix-dwarf-rev0, Built by MinGW-W64 project) 编译 GCC 会得到以下时间:

Elapsed: 0.019841
Elapsed: 0.0099643
Elapsed: 0.0094552

PC 2

我使用 Visual Studio,仍然使用 /O2

Elapsed: 0.054841
Elapsed: 0.03543
Elapsed: 0.034552

我没有在 PC 2 上执行 Clang 和 GCC,但改进不足以解决我的担忧。

魔杖盒

问题在于 Wandbox (https://wandbox.org/permlink/VIvardJNAMKzSbMf) 上完全相同的代码执行速度要快 10-80 倍

Elapsed: 0.00115457
Elapsed: 0.000815412
Elapsed: 0.000814636

Wandbox 使用 GCC 10.0.0 和 c++14。我意识到它很可能在 linux 上运行,但我找不到任何方法让 GCC 10 在 Windows 上编译,所以我无法使用该版本测试编译。

C# - 快 200 倍

这是对我编写的 C# 应用程序的重写,它的运行速度要快得多:

Elapsed: 0.017424 
Elapsed: 0.0006065 
Elapsed: 0.000733 
Elapsed: 0.0006166 
Elapsed: 0.0004699 

Finished Parsing: 100 Records. Elapsed :0.0082796 at a rate of : 12076/s

C# 方法如下所示:

Stopwatch sw = new Stopwatch();
sw.Start();
foreach (dynamic item in TableData.data)  //TableData is a JSON file with the structure definition
{

    string DataType = item.DataType;
    int startingPosition = item.StartingPosition;

    int width = Convert.ToInt32(item.ColumnWidth);
    if (width+startingPosition >= FullLineLength)
    {
        continue;
    }

    byte[] currentColumnBytes = currentLineBytes.Skip(startingPosition).Take(width).ToArray();

   // .....     200 extra lines of processing into ints, dates, strings       ......
   // ..... Even with the extra work, it operates at 1200+ records per second ......

}
sw.Stop();
var seconds = sw.Elapsed.TotalSeconds;
sw.Reset();
Console.WriteLine("Elapsed: " + seconds);
TempTable.Rows.Add(dataRow);

当我开始这样做时,我期望通过将代码从 C# 移动到非托管 C++ 来获得巨大的性能提升。这是我的第一个 C++ 项目,坦率地说,我对自己所处的位置感到有些沮丧。可以做些什么来加速这个 C++?我是否需要使用不同的数据类型、malloc、更多/更少的结构?

它需要在 Windows 上运行,不确定是否有办法让 GCC 10 在 Windows 上运行?

您对有抱负的 C++ 开发人员有什么建议?

【问题讨论】:

  • 差异很可能是由于 C++ 的价值取向与 C# 的参考取向。如果您通过值传递具有堆中部分的大型对象,则与 C# 相比,事情将非常慢。特别是如果没有使用移动语义。在 C++ 中传递这些时使用 const T&amp;T&amp; 如果你正在改变数据。
  • 有关 C++ 程序优化的更多信息,可以在 pdf here 找到一个不错的参考。通常,您希望通过引用或 const 引用传递对象,并通过值传递普通数据类型(例如 int)。第 7 章的大部分内容,尤其是 7.12-7.15 节都有更多关于不同数据类型、函数和传递的效率的信息。
  • 代码中有几件事让我印象深刻。向量是一块一块地建立起来的,这会造成很多颠簸。最好在申报时预先分配。此外,ASCII 十六进制字节的转换将比糖蜜慢。您可能会考虑编写一个简短的函数来转换 2 char he 值。应该跑得快很多。在这种情况下,标准转换函数非常糟糕。
  • 除上述之外。您用于 JSON 的库并不是特别快github.com/miloyip/nativejson-benchmark#parsing-time 许多编译器。我怀疑 C# 的 newtonsoft JSON 更快。 @SamVarshavchik 你有这方面的证据吗?微软在 MSVC 上投入了大量时间,他们在 STL devblogs.microsoft.com/cppblog/open-sourcing-msvcs-stl 方面也做出了很多贡献。我认为您不会找到任何证据,因为微软并不是唯一为 Windows 生产编译器的公司。 Clang、GCC 和 Intel 都生产编译器。
  • C++ 的优点之一是您正在编程接近金属。但是当你超出范围时,诸如析构函数之类的东西会自动点击。这对于控制堆使用非常有用,但 C# 使用垃圾收集,并且可能会在对象不再使用后很长时间延迟堆清理。收集最终会出现相关的延迟,但您几乎无法控制。这很好,也很糟糕。只是在编码,尤其是基准测试中需要注意的事情。

标签: c# c++ gcc


【解决方案1】:

这实际上取决于在汇编程序/机器代码中执行的命令。 VS 在 C++ 方面从未表现出色,多年来,Borland 在效率和可靠性方面都表现出色。然后,Borland 将他们的 IDE 和 C++ 分支作为一家独立的公司出售。

这还取决于您如何在 C++ 中编写该过程,您能否编辑以显示该代码?

C# 的优势在于它是可管理的,并且可以对代码使用更高级别的解释,因此在后台它可以 JIT 代码将整行转换为已解析的格式,然后 for 循环将块断开(循环 1 步),如果你用 C++ 编写它,即使效率较低,它也会更准确地遵循你的命令,即:它会断开你正在查看的块,然后将其转换为解析后的格式(循环 2 步)

所以使用上面的例子,如果我们假设 2 个命令加起来比 C++ 中的 2 个命令慢 50%,但是这 2 个命令在每个循环中都被处理,而 c# 代码在每个循环中只处理 1 个命令,任何效率低下都会加剧。

在上面的 cmets 中也 +1,参考与价值可以产生相当大的差异,尤其是在处理大型数据集时。我认为他的答案最有可能导致巨大的差异。

简化是我相信的答案:

std::string byteString = hex.substr(i, 2);
unsigned char byte = (unsigned char) strtol(byteString.c_str(), NULL, 16);

可以变成

unsigned char byte = (unsigned char) strtol(hex.substr(i, 2).c_str(), NULL, 16);

并删除次要内存分配。但是同样,如果您可以将整个源转换为字节流,然后在其上使用 for 循环,您就可以从循环中删除转换步骤。

【讨论】:

  • 感谢 Slipoch,您是否看到完整 C++ 代码的链接:wandbox.org/permlink/VIvardJNAMKzSbMf?相关行在48-85的主页上
  • 是的,刚刚看到,我肯定会考虑在任何函数调用中以及在处理数据时使用引用而不是值。我还建议简化流程的每个部分,或者看看是否可以将其删除。
  • 谢谢,我一定会的。秒表没有计时那个特定区域,但这个想法仍然很棒。我从来没有使用过指针,但希望今晚能学会如何使用它们。
  • 啊,我以前擅长指点。使用 C# 这么久了,我几乎忘记了使用它们的所有信息。
【解决方案2】:

好的,所以我能够让 C++ 以每秒大约 50,000 行的速度处理文件,每行 80 列。我重新设计了整个工作流程,以确保它根本不需要回溯。我首先将整个文件读入ByteArray,然后将数据从一个数组移动到另一个数组,而不是在for 循环中指定每个字节,从而逐行遍历它。然后我使用map 来存储数据。

    stopwatch<> sw;
    while (CurrentLine < TotalLines)
    {
        int BufferOffset = CurrentLine * LineLength;
        std::move(ByteArray + BufferOffset, ByteArray + BufferOffset + LineLength, LineByteArray);
        for (int i = 0; TotalColumns > i + 1; ++i)
        {
            int ThisStartingPosition = StartingPosition[i];
            int ThisWidth = ColumnWidths[i];
            std::uint8_t* CurrentColumnBytes;
            CurrentColumnBytes = new uint8_t[ThisWidth];
            {
                std::move(LineByteArray + ThisStartingPosition, LineByteArray + ThisStartingPosition + ThisWidth, CurrentColumnBytes);
                ResultMap[CurrentLine][i] = Format(CurrentColumnBytes, ThisWidth, DataType[i]);
            }
        }
        CurrentLine++;
    }
    std::cout << "Processed" << CurrentLine << " lines in : " << duration_cast<double>(sw.elapsed()) << '\n';

我还是有点失望,因为使用 Clang 编译无法使用 Boost 公历转换,并且使用标准的 MS 编译器使其慢了近 20 倍。使用Clang -O3,它在0.25 秒内处理了10,700 条记录,包括所有intstring 转换。我只需要编写自己的date 转换。

【讨论】:

    猜你喜欢
    • 2013-10-31
    • 1970-01-01
    • 2016-03-14
    • 1970-01-01
    • 2014-10-10
    • 2015-09-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多