为什么在C ++中这么慢?

我已经将这个简单的方法从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>()); 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 begin(ss), end; pathLookupVectors[i].push_back(vector(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>()); for (int j = 0; j < nodes.Count; j++) { string str = streamReader.ReadLine(); pathLookupLists[i].Add(new List()); //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实例并在完成的同时运行单个方法程序很快。 我意识到问题可能是一些基本的设置,但我没有经历过如此道歉,如果这确实令人失望地成为原因。 我仍然不知道为什么要花这么长时间。

根据您的更新,很明显您自己发布的function不会导致性能问题,因此虽然有很多方法可以优化它,但似乎没有帮助。

我认为每次运行代码都可以重现这个性能问题,对吗? 然后我建议你做以下测试:

  • 如果你是在调试模式下编译程序(即没有优化),那么重新编译发布(完全优化,支持速度),看看是否有所作为。

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

  • 使用源代码分析器确定所花费的时间。 如果调用此函数与否之间的差异是几分钟,那么分析器肯定会给出发生了什么的线索。 如果你在Windows上,那么Very Sleepy是一个不错的选择,如果你在Linux上,你可以使用OProfile 。

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

我确信您需要调试应用程序的其他部分,并且您不希望等待所有这些时间才能在调试模式下完成此function。 此问题的解决方案是在发布模式下构建项目,但以下列方式更改发布配置:

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

  • 启用要生成的调试信息(pdb文件)。 为此,请选择Solution Explorer顶部的Project,右键单击,选择Properties。 然后转到Configuration Properties | Linker | Debugging并将Debug构建中的所有设置复制到Release构建中。

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

完成调试后,您需要重新设置所有这些设置。

我希望这有帮助。

我用Very Sleepy ( Visual C ++ 2010,32位Windows XP)分析了代码。 我不知道我的输入数据有多相似,但无论如何这里都是结果:

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

12%basic_iostream :: basic_iostream

9%的运营商+

8%_Mutex :: Mutex

5%的getline

5%basic_stringbuf :: _ Init

4%locale :: _ Locimp :: _ Addfac

4%vector :: reserve

4%basic_string :: assign

3%运营商删除

2%basic_Streambuf :: basic_streambuf

1%Wcsxfrm

5%的其他function

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

代码中的while循环看起来非常混乱和冗长,因为它以不需要的方式执行操作:

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

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

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

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

这取自@David的评论。

或者甚至更好,如果你这样做,当你push_back向量本身:

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

完成!

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


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

 void Level::PopulatePathVectors(const string &pathTable) 

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


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

 tempString.append(line[count]); 

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

有几个地方可以做到这一点,比如使用tempString

以下是我没有看到的其他人提到的一些事情。 它们有点模糊,但无法重现事物使得很难详细说明所有内容。

穷人的剖析。

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

开始评论东西。 如果你注释掉你的分裂并立即完成,那么它非常清楚从哪里开始。

有些代码是依赖的,但你可以将完整的文件读入内存,然后进行解析,以便在花费时间的地方创建明显的分离。 如果两者都是独立完成的,那么它可能就是互动。

缓冲。

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

虽然它看起来不像你在这里写,但你的主程序可能有更多的内存被使用。 在达到最高水位后,操作系统可能会开始将部分内存分页到磁盘。 当您在分页发生时逐行阅读时,您会捶打。

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

保留。

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

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

最后一点是有争议的,尽管我已经读过了。 我没有理由怀疑我错了,虽然我将不得不做更多的研究来确认或否认。

不过,如果你使用reserve,push_back可以运行得更快。

字符串拆分。

在处理gb +文件时,我从未见过一个高性能的C ++迭代器解决方案。 不过,我没有特别试过那个。 我猜测为什么他们倾向于做很多小额分配。

这是我通常使用的参考。

将字符数组拆分为两个字符数组

关于std::vector::reserve适用于此处。

我更喜欢使用boost::lexical_cast来实现维护问题的实现,尽管我不能说它的性能比流实现更多或更少。 我会说实际上看到对流使用的正确错误检查是非常罕见的。

STL诡计。

抱歉,我故意对这些模糊不清。 我经常编写避免这些条件的代码,尽管我确实记得同事告诉我的一些试验和磨难。 使用STLPort可以完全避免这些问题。

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

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

某些容器有奇怪的性能特征。 我不会遇到vector的问题,但我真的不知道istream_iterator内部使用了什么。 在过去,我已经通过一个行为不当的算法来查找std::list::size调用,例如使用GCC对列表进行完全遍历。 我不知道新版本是否不那么疯狂。

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

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

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

C ++ 11中引入的移动语义应该可以消除这种影响。 在此之前,尝试vector< shared_ptr< vector > >list< vector > ,或者,如果您事先知道未来的大小,请使用vector::reserve()来避免重新分配。

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

考虑使用std::dequestd::list或其他具有更好增长行为的容器。 有关详细信息,请参阅此文章 。

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

由于你的函数本身并不慢1 ,程序运行缓慢的原因必须是当填充了pathLookupVectors时,使用此函数乘积的某些代码会变慢。

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

1.建立在您最新的编辑中。