【问题标题】:Why is this so much slower in C++?为什么这在 C++ 中这么慢?
【发布时间】:2011-10-18 15:06:51
【问题描述】:

我已将这个简单的方法从 C# 转换为 C++。它读取路径表并填充整数列表(或整数向量的向量)列表。

路径表中的示例行类似于

0 12 5 16 n

我意识到一般来说有更好的方法来做到这一点,但现在我只想知道为什么我的 C++ 代码花费的时间如此要长得多。例如10 分钟而不是 C# 版本的 10 秒。这是我的 C++ 代码。我猜我做错了什么。

//Parses the text path vector into the engine
void Level::PopulatePathVectors(string pathTable)
{
    // Read the file line by line.
    ifstream myFile(pathTable);

        for (unsigned int i = 0; i < nodes.size(); i++)
        {
            pathLookupVectors.push_back(vector<vector<int>>());

            for (unsigned int j = 0; j < nodes.size(); j++)
            {
                string line;

                if (getline(myFile, line)) //Enter if a line is read successfully
                {
                    stringstream ss(line);
                    istream_iterator<int> begin(ss), end;
                    pathLookupVectors[i].push_back(vector<int>(begin, end));
                }
            }
        }
    myFile.close();
}

这里是 C# 版本:

private void PopulatePathLists(string pathList)
{
    // Read the file and display it line by line.
    StreamReader streamReader = new StreamReader(pathList);

    for (int i = 0; i < nodes.Count; i++)
    {
        pathLookupLists.Add(new List<List<int>>());

        for (int j = 0; j < nodes.Count; j++)
        {
            string str = streamReader.ReadLine();
            pathLookupLists[i].Add(new List<int>());

            //For every string (list of ints) - put each one into these lists
            int count = 0;
            string tempString = "";

            while (str[count].ToString() != "n") //While character does not equal null terminator
            {
                if (str[count].ToString() == " ") //Character equals space, set the temp string 
                                                  //as the node index, and move on
                {
                    pathLookupLists[i][j].Add(Convert.ToInt32(tempString));
                    tempString = "";
                }
                else //If characters are adjacent, put them together
                {
                    tempString = tempString + str[count];
                }
                count++;
            }
        }
    }
    streamReader.Close();
}

对不起,这太具体了,但我很难过。

编辑 - 很多人都说他们已经测试过这段代码,而且对他们来说只需要几秒钟。我所知道的是,如果我注释掉对这个函数的调用,程序会在几秒钟内加载。使用函数调用需要 5 分钟。几乎一模一样。我真的很难过。可能是什么问题?

这是它正在使用的PathTable

编辑 - 我尝试在程序中单独运行该函数,但花了几秒钟,但恐怕我知道的不够多,无法知道如何解决这个问题。显然这不是代码。会是什么呢?我检查了它被调用的位置,看看是否有多个调用,但没有。它在游戏关卡的构造函数中,并且只被调用一次。

编辑 - 我知道代码不是最好的,但这不是重点。它自己运行很快 - 大约 3 秒,这对我来说很好。我试图解决的问题是为什么在项目内部需要这么长时间。

编辑 - 我注释掉了除主游戏循环之外的所有游戏代码。我将该方法放入代码的初始化部分,该部分在启动时运行一次。除了一些设置窗口的方法外,它现在与只有方法的程序几乎相同,只是它仍然需要大约 5 分钟才能运行。现在我知道它与 pathLookupVectors 的依赖无关。另外,我知道计算机开始写入硬盘驱动器不是内存问题,因为当慢速程序正在运行该方法时,我可以打开另一个 Visual Studio 实例并同时运行单个方法程序,这样就完成了马上。我意识到问题可能是一些基本设置,但如果这确实令人失望地最终成为原因,我没有经历过如此道歉。我仍然不知道为什么要花这么长时间。

【问题讨论】:

  • 你在编译优化吗?
  • 为什么C++版本用str[count]!='\n',第二个用str[count].ToString()!="\n"?
  • @MikeSeymour 我不认为优化将占 C++ 方法所花费的所有 9 分 50 秒的额外时间。您是否尝试过调试并比较执行流程?我的猜测是这样的差异存在某种逻辑问题导致 C++ 方法循环更多。
  • 只是猜测:size() 在您使用的集合类型中是 O(1) 还是 O(n) 操作?你对它进行 O(n^2) 调用,如果它们都是 O(n),那么这就是 O(n^3) 的总成本,这是相当多的。但是,与其从互联网上征求随机猜测,不如通过分析器运行代码并自己回答问题,看看它说了什么?
  • @SirYakalot:您介意发布两个项目的完整代码吗?我想看看这个完整的上下文。当然不是在这里,而是在某个文件托管站点,只需给我链接。虽然不承诺任何结果。

标签: c# c++ io


【解决方案1】:

我使用Very SleepyVisual C++ 2010,32 位 Windows XP)分析了代码。我不知道我的输入数据有多相似,但无论如何,结果如下:

39% 的时间花在 basic_istream::operator>>

12% basic_iostream::basic_iostream

9% 运算符+

8% _Mutex::Mutex

5% 获取线

5% basic_stringbuf::_Init

4% 语言环境::_Locimp::_Addfac

4% 向量::reserve

4% basic_string::assign

3% 运算符删除

2% basic_Streambuf::basic_streambuf

1% Wcsxfrm

5% 其他功能

有些东西似乎来自内联调用,所以很难说它实际上来自哪里。但你仍然可以得到这个想法。这里唯一应该做 I/O 的是 getline,它只需要 5%。其余的是流和字符串操作的开销。 C++ 流非常慢。

【讨论】:

  • 这太棒了,非常感谢。那么 basic_istream::operator>> 到底是什么意思呢?那是指哪几行?解析文件的所有内容?
  • @SirYakalot:至少列表中的前两个调用来自stringstream(tempString) &gt;&gt; result;。您构造流对象,然后使用 operator>> 来读取数据。如果您想要更快的字符串到 int 的转换,请改用 C 函数 atoiatol,或为此目的优化的一些外部库,例如 StrTk。
  • C++ 流并不慢。缓慢的是 Dinkumware 的实现(VS 附带的那个)。将整数写入流涉及五个锁,其中四个是全局。标准都没有强制要求。
【解决方案2】:

根据您的更新,很明显您发布的功能本身不会导致性能问题,因此虽然有很多方法可以优化它,但似乎无济于事。

我认为您可以在每次运行代码时重现此性能问题,对吗?那我建议你做以下测试:

  • 如果您在调试模式下编译程序(即没有优化),然后重新编译以发布(完全优化,有利于速度),看看是否有区别。

  • 要检查是否在此可疑函数上花费了额外的时间,您可以在函数的开头和结尾添加包含时间戳的 printf 语句。如果这不是一个控制台应用程序而是一个 GUI 应用程序,并且 printfs 没有去任何地方,那么写入一个日志文件。如果您在 Windows 上,您也可以使用 OutputDebugString 并使用调试器来捕获 printfs。如果您使用的是 Linux,则可以使用 syslog 写入系统日志。

  • 使用源代码分析器来确定所有时间都花在了哪里。如果调用或不调用这个函数之间的区别是几分钟,那么分析器肯定会提供关于正在发生的事情的线索。如果您使用的是 Windows,那么Very Sleepy 是一个不错的选择,如果您使用的是 Linux,则可以使用 OProfile

更新:所以你说发布版本很快。这可能意味着您在此函数中使用的库函数的调试实现速度较慢。众所周知,STL 就是这样。

我确定您需要调试应用程序的其他部分,并且您不想等待所有这些分钟才能在调试模式下完成此功能。这个问题的解决方法是在发布模式下构建你的项目,但是通过以下方式更改发布配置:

  • 仅对您要调试的文件禁用优化(确保至少对具有慢速功能的文件保持启用优化)。要禁用文件优化,请在解决方案资源管理器中选择文件,右键单击,选择属性,然后转到配置属性|C/C++/优化。查看该页面中的所有项目是如何为 Debug 构建设置的,然后将所有项目复制到您的 Release 构建中。对您希望调试器可用的所有文件重复此操作。

  • 启用要生成的调试信息(pdb 文件)。为此,请选择解决方案资源管理器顶部的项目,右键单击,选择属性。然后转到配置属性|链接器|调试并将调试版本中的所有设置复制到发布版本中。

通过上述更改,您将能够像在调试版本中那样调试如上配置的发布二进制文件部分。

一旦您完成调试,您当然需要重新设置所有这些设置。

我希望这会有所帮助。

【讨论】:

  • 重新编译发布速度很快,唯一的问题是(我知道这听起来很明显)但这不意味着我没有调试它吗?我需要它足够快,这样我每次更改代码并想查看结果时都不必等待 5 分钟。显然这并不总是可能的,但在它自己的项目中它很快,所以我只是不知道为什么它在这里也不快。
  • 那么这就是您问题的答案。您必须使用一些在调试版本中速度较慢的库函数。调试 STL 特别慢,因为它添加了很多检查和断言。请参阅我的答案中的编辑以了解如何解决您的问题。
  • 所以这会将发布配置变成调试某些部分的配置?
  • 另外,很抱歉成为这样的菜鸟,但我究竟该如何进行这些更改?我不知道在哪里看!
  • 我也认为 Miguel 正在做某事。我自己经常注意到代码错误位置的断点(尤其是条件断点)会使代码非常变慢。检查以确保您在任何地方都没有任何断点,但鉴于您似乎对 Visual Studio 没有经验,我猜您没有太多断点?
【解决方案3】:

您代码中的whileloop 似乎非常混乱且冗长,因为它以一种不需要的方式做事:

一个简单而快速的等效代码是这样的:

int result;
stringstream ss(line);
while ( ss >> result ) //reads all ints untill it encounters non-int
{
    pathLookupVectors[i][j].push_back(result);
}

在 C++ 中,这样的循环也是惯用的。或者代替这个手动循环,你可以写 use std::copy 1:

std::copy(std::istream_iterator<int>( ss ), 
          std::istream_iterator<int>(), 
          std::back_inserter(pathLookupVectors[i][j]));

1.它来自@David 的评论。

如果你这样做会更好,当你 push_back 向量本身时:

 if (getline(myFile, line)) //enter if a line is read successfully
 {
   stringstream ss(line);
   std::istream_iterator<int> begin(ss), end;
   pathLookupVectors[i].push_back(vector<int>(begin, end));
 }

完成!

【讨论】:

  • 啊,但这意味着每个元素将包含例如 - 0 2 4 8 n。我希望每个元素都简单地为 0 然后下一个包含 2 然后 4 等等。
  • @SirYakalot:你想说什么?这段代码首先读取0,然后是2,然后是4,然后是8,然后在遇到n时退出循环。
  • 或者std::copy( std::istream_iterator&lt;int&gt;( ss ), std::istream_iterator&lt;int&gt;(), std::back_inserter( pathLookupVectors[i][j] ) ); 用于单线
  • @DavidRodríguez-dribeas:那就更好了。让我将此添加到我的答案中。
  • @SirYakalot:如果您正在运行调试 C++ 迭代器,由于边界和溢出检查等原因,迭代器会很慢。在优化发布时,上述 Nawaz 的任何选项都应该像闪电一样。
【解决方案4】:

我不确定这里发生了什么,但我看到了一些可以优化代码的方法。如果这不能让你到达那里,那么可能还有其他事情发生。


你的琴弦有多大?当您在 C++ 版本中传递它们时,您正在制作副本,因为您是“按值传递”。尝试通过常量引用传递它:

void Level::PopulatePathVectors(const string &pathTable)

这通过引用传递对象,这意味着它不会复制。然后,习惯上将其设为 const 以确保它不会在您的函数中被修改。


使用.append+= 扩展tempString。我相信您正在创建一个新的字符串对象,然后用 + 替换旧的对象,而 +=.append 将修改当前的对象:

tempString.append(line[count]);

您还可以通过在顶部声明变量然后重新分配给它们来调整性能。这将防止它们每次都被重新创建。例如,将 string line; 放在 for 循环之前,因为无论如何它都会被覆盖。

有几个地方可以做到这一点,例如tempString

【讨论】:

  • +1,str=str+char -> str.append(char) 是一个很大的改进(需要的对象和内存分配要少得多)
【解决方案5】:

这里有一些我没有看到其他人提到过的事情。它们有些模糊,但由于无法重现事物,因此很难详细说明所有内容。

穷人的画像。

在代码运行时,继续打断它。通常你会一遍又一遍地看到相同的堆栈帧。

开始评论内容。如果您注释掉拆分并立即完成,那么从哪里开始就很清楚了。

有些代码是依赖的,但您可以将整个文件读入内存,然后进行解析以明确区分其花费时间的位置。如果两者都独立快速完成,那么它可能是交互。

缓冲。

我没有看到您的读取有任何缓冲。如果您将任何内容写入磁盘,这一点就变得尤为重要。磁盘上的手臂会在读取位置和写入位置之间来回跳转,等等。

虽然看起来不像您在此处编写,但您的主程序可能使用了更多内存。有可能在达到最高水位后,操作系统开始将一些内存分页到磁盘。当您在分页发生时逐行阅读时,您会颠簸。

通常,我会设置一个简单的迭代器接口来验证一切是否正常。然后在它周围写一个装饰器,一次读取 500 行。标准流也内置了一些缓冲选项,这些选项可能更好用。我猜他们的缓冲默认值相当保守。

预留。

std::vector::push_back 在同时使用 std::vector::reserve 时效果最佳。如果您可以在进入紧密循环之前使大部分内存可用,那么您就赢了。您甚至不必确切知道多少,只需猜测即可。

你实际上也可以用这个来击败std::vector::resize的性能,因为std::vector::resize使用allocstd::vector::push_back将使用realloc

最后一点是有争议的,虽然我读过其他的。我没有理由怀疑我错了,尽管我需要做更多的研究来确认或否认。

尽管如此,push_back 可以在你使用 Reserve 的情况下运行得更快。

字符串拆分。

我从未见过在处理 gb+ 文件时表现出色的 C++ 迭代器解决方案。不过,我还没有具体尝试过。我的猜测是,他们倾向于进行大量小额分配。

这里是我常用的参考。

Split array of chars into two arrays of chars

std::vector::reserve 的建议适用于此处。

出于维护问题,我更喜欢boost::lexical_cast 流式实现,尽管我不能说它比流式实现的性能更高或更低。我会说,实际上很少看到对流使用情况进行正确的错误检查。

STL 恶作剧。

我故意含糊其辞,抱歉。我通常编写代码来避免这些条件,尽管我确实记得同事告诉我的一些考验和磨难。使用 STLPort 可以完全避免其中很大一部分。

在某些平台上,使用流操作会默认启用一些奇怪的线程安全。所以我看到次要的 std::cout 使用绝对会破坏算法的性能。您在这里没有任何东西,但是如果您在另一个线程中进行日志记录,可能会造成问题。我在另一条评论中看到8% _Mutex::Mutex,这可能说明它的存在。

退化的 STL 实现甚至可能在词法解析流操作中出现上述问题,这似乎是合理的。

某些容器有奇怪的性能特征。我从来没有遇到过矢量问题,但我真的不知道 istream_iterator 在内部使用什么。过去,我通过一种行为不端的算法进行了跟踪,以找到一个 std::list::size 调用,例如,它使用 GCC 对列表进行了完整的遍历。我不知道新版本是否不那么空洞。

通常愚蠢的 SECURE_CRT 愚蠢应该被愚蠢地照顾。我想知道这是否是微软认为我们想要花时间做的事情?

【讨论】:

  • (1) realloc 不能(有用地)用于对象数组。因此,std::vector 不使用 realloc。 (2) 根据stackoverflow.com/questions/228908/is-listsize-really-on,GCC 是具有线性std::list 的那个。这在理论上使std::splice 更快,这是使用列表的主要原因。另外,请改用list::empty()。 (3) 从其他帖子来看,这是 Dinkumware 的恶作剧,因为他使用的是调试版本。 (4) STL != C++ 标准库
  • “穷人的画像”是Mike Dunlavey's method
【解决方案6】:

List.Addvector::push_back 都会随着容器的增长不时重新分配内存。 C++ 向量按值存储子向量,因此它们的所有数据(在您的情况下似乎很大)被一次又一次地复制。相比之下,C# list 通过引用存储子列表,因此在重新分配期间不会复制子列表的数据。

典型的向量实现在重新分配期间使其容量翻倍。因此,如果您有 100 万行,子向量将被复制 log(2,1000000) ≈ 10 次。

C++11 中引入的

移动语义 应该可以消除这种影响。在此之前,请尝试 vector&lt; shared_ptr&lt; vector&lt;int&gt; &gt; &gt;list&lt; vector&lt;int&gt; &gt;,或者,如果您提前知道未来的大小,请使用 vector::reserve() 以避免重新分配。

【讨论】:

  • 对不起,我是个新手。我如何以及为什么要使用您建议的两条线?提前致谢!
  • 这是我在评论中询问编译器/版本的主要原因。 VS 的 STL(从 Dinkumware 获得许可)通过一些巧妙的元编程技巧有效地增长 std::vector&lt;std::vector&lt;T&gt;&gt;:检测包含的类型具有有效的 swap 操作,分配空向量的新缓冲区(而不是复制),并使用 @ 987654328@ 将原始缓冲区中的向量移动到新缓冲区。 std::vector&lt;&gt;::swap 通常需要 3 次指针交换(9 条汇编指令——同样,取决于 STL 并定义它可能需要更多,特别是在 DEBUG 中),这很有效
  • @SirYakalot 最快的方法是将pathLookupVectors 类型替换为向量列表或向量的双端队列。如果没有看到其余代码及其使用方式,很难提出更多建议。
【解决方案7】:

尚未测试代码,但它通常会加载多少 ints?考虑当您的每个vectors 到达其capacity 时会发生什么。 vector 的增长效率低下 - 我相信 O(n)。 C# 的 List 没有这种行为。

考虑使用std::dequestd::list 或其他一些具有更好增长行为的容器。请参阅此article 了解更多信息。

【讨论】:

  • 主向量中有744个向量,每个向量包含另外744个整数向量。所以......大约一百万个整数。
  • A vector 的增长可能效率低下,但其摊销的 big-O 复杂度为 O(1),而不是 O(n)。
  • 哎哟。您受到重新分配/增长行为的打击。作为一个快速测试,看看这个假设是否正确,尝试用lists 替换所有vectors。
  • @Rob 不需要将每个元素复制到新的内部缓冲区吗?使它成为 O(n)?
  • 是的,可以,但不必每次都这样做。当您将vector 增长到push_back 时,它只会偶尔重新分配,然后大量重新分配。 C++ 标准要求来自push_back() 的分期 O(1) 行为。
【解决方案8】:

如果你有非常多的元素,每次向量被推回时,你都会受到重新分配和复制的惩罚。尝试在 C++ 中使用不同的容器。

【讨论】:

  • 问题是向量是我在程序中使用后最好的东西。
  • @SirYakalot 你能将向量预分配到某个最大尺寸吗?
  • 我刚刚在下面看到您知道向量的最大大小。然后,为最大大小预先分配整个向量并观察差异。
【解决方案9】:

由于你的函数本身并不慢1,所以程序慢的原因一定是在填充pathLookupVectors后,一些使用该函数乘积的代码变慢了。 p>

我认为在您的程序上运行分析器是找出问题的最佳方法,但您也可以查看您的代码并找到依赖于 pathLookupVectors 的每一段代码,并考虑它是否可能是您的瓶颈正在寻找。

1。在您最近的编辑中建立。

【讨论】:

  • 这是一个非常好的主意,不幸的是,在此构建中没有实例化或运行任何依赖于查找向量的内容。很奇怪!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-10-26
  • 1970-01-01
  • 2021-09-03
  • 1970-01-01
  • 1970-01-01
  • 2014-08-27
  • 2014-05-17
相关资源
最近更新 更多