为什么在c++中这么慢?
本文关键字:c++ 为什么 | 更新日期: 2023-09-27 18:11:49
我已经将这个简单的方法从c#转换为c++。它读取一个路径表,并填充一个int型列表的列表(或int型向量的向量的向量)。
路径表中的样例行类似于
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();
}
对不起,这太具体了,但我被难住了。
EDIT -很多人说他们已经测试了这段代码,对他们来说只需要几秒钟。我只知道,如果我注释掉对这个函数的调用,程序会在几秒钟内加载。函数调用需要5分钟。几乎完全。我真的被难住了。问题是什么呢?
这是它使用的PathTable
编辑-我试着在一个程序中运行这个函数,它花了几秒钟,但恐怕我知道的不够,无法知道如何解决这个问题。显然不是代码的问题。会是什么呢?我查了它被调用的位置看看是否有多个调用,但没有。它存在于游戏关卡的构造函数中,并且只被调用一次。
编辑-我知道代码不是最好的,但这不是重点。它自己运行得很快——大约3秒,这对我来说很好。我想解决的问题是为什么在项目中需要这么长时间。
编辑-我注释了所有的游戏代码,除了主要的游戏循环。我将该方法放入代码的初始化部分,该部分在启动时运行一次。除了设置窗口的几个方法之外,它现在几乎与只有方法的程序相同,只是它仍然需要大约5分钟才能运行。现在我知道它与pathLookupVectors的依赖关系无关。此外,我知道这不是内存的事情,计算机开始写入硬盘驱动器,因为当缓慢的程序正在运行方法时,我可以打开另一个Visual Studio实例并同时运行单个方法程序,这在几秒钟内完成。我意识到问题可能出在一些基本设置上,但我没有经验,所以如果这确实是令人失望的最终原因,我很抱歉。我还是不明白为什么要花这么长时间。
我使用Very Sleepy (Visual c++ 2010, 32位Windows XP)对代码进行了分析。我不知道我输入的数据有多相似,但结果如下:
39%的时间花在basic_istream::operator>>
12% basic_iostream:: basic_iostream
操作符+ 9%
8% _Mutex::互斥getline
5%
5% basic_stringbuf:: _Init
4%地区:_Locimp:: _Addfac
4%向量::储备
4% basic_string::分配
3% operator delete
2% basic_Streambuf:: basic_StreambufWcsxfrm
1%
5%其他功能
有些东西似乎来自内联调用,所以很难说它实际上来自哪里。但你还是可以理解的。这里唯一需要做I/O的是getline,它只占用5%的时间。其余部分来自流和字符串操作的开销。c++流慢得要命
根据您的更新,很明显,您自己发布的功能不会导致性能问题,因此,虽然有很多方法可以优化它,但似乎不会有帮助。
我想你每次运行代码时都会重现这个性能问题,对吗?那么我建议您做以下测试:
-
如果你在调试模式下编译你的程序(即没有优化),然后重新编译发布(完全优化,有利于速度),看看这是否有区别。
-
要检查是否在这个可疑的函数上花费了额外的时间,您可以在函数的开始和结束处添加printf语句,其中包括时间戳。如果这不是一个控制台应用程序,而是一个GUI应用程序,并且printfs没有去任何地方,那么就写入日志文件。如果您在Windows上,您可以选择使用OutputDebugString并使用调试器来捕获打印。如果是Linux系统,可以使用syslog命令写入系统日志。
-
使用源代码分析器来确定在哪里花费了所有的时间。如果调用这个函数或不调用这个函数之间的差异是几分钟,那么分析器肯定会给出关于正在发生的事情的线索。如果你是在Windows上,那么Very Sleepy是一个不错的选择,如果你是在Linux上,你可以使用OProfile。
Update:所以你说一个发布版本是快速的。这可能意味着您在此函数中使用的库函数具有缓慢的调试实现。STL是这样的
我确信您需要调试应用程序的其他部分,并且您不希望在调试模式下等待所有这些时间来完成此功能。这个问题的解决方案是在发布模式下构建项目,但以以下方式更改发布配置:
-
仅对要调试的文件禁用优化(确保至少对具有慢速功能的文件保持启用优化)。要禁用对文件的优化,请在解决方案资源管理器中选择该文件,右键单击,选择属性,然后转到配置属性|C/c++/优化。查看该页中的所有项是如何为调试构建设置的,并复制发布构建中的所有项。
-
使能生成调试信息(pdb文件)。为此,选择解决方案资源管理器顶部的项目,右键单击,选择属性。然后进入配置属性|链接器|调试和复制所有的设置从调试版本到发布版本。
通过上述更改,您将能够调试发布二进制文件中如上配置的部分,就像您在调试构建中所做的那样。
一旦调试完成,你当然需要重置所有的设置。
我希望这对你有帮助。
代码中的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<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));
}
完成了!
我不太确定这里发生了什么,但我看到了一些可以优化代码的方法。如果这不能让你达到目的,那么可能还有别的事情在发生。
你的字符串有多大?当你在c++版本中传递它们时,你是在做拷贝,因为你是"按值传递"。尝试通过常量引用传递它:
void Level::PopulatePathVectors(const string &pathTable)
通过引用传递对象,这意味着它没有创建副本。然后,通常将其设置为const
,以确保它不会在您的函数中被修改。
使用.append
或+=
扩展tempString
。我相信你正在制作一个新的字符串对象,然后用+
替换旧的字符串对象,而+=
和.append
将修改当前对象:
tempString.append(line[count]);
您还可以通过在顶部声明变量然后重新赋值来调整性能。这将防止它们每次都被重新创建。例如,将string line;
放在For循环之前,因为它无论如何都会被覆盖。
有几个地方可以这样做,比如tempString
。
这里有一些我没有看到其他人提到的事情。它们有些模糊,但由于无法复制事物,因此很难详细说明所有内容。
当代码正在运行时,只要不停地打断它。通常你会一遍又一遍地看到相同的堆栈帧。
开始注释。如果你注释掉你的拆分,它立即完成,那么从哪里开始就很清楚了。
有些代码是依赖的,但是您可以将整个文件读取到内存中,然后进行解析,以创建一个明显的隔离它在哪里花费时间。如果两者都很快独立完成,那么很可能是交互。
缓冲。
我没有看到你的读取有任何缓冲。如果要向磁盘写入任何内容,这一点尤为重要。磁盘上的机械臂将在读取位置和写入位置之间来回跳动,等等。
虽然它看起来不像你在这里写,你的主程序可能有更多的内存被使用。有可能在达到最高值后,操作系统开始将一些内存分页到磁盘。当分页发生时逐行读取时,会出现抖动
通常,我会设置一个简单的迭代器接口来验证一切正常。然后在它周围写一个装饰器,一次读取500行。标准流也内置了一些缓冲选项,使用起来可能会更好。我猜他们的缓冲默认值是相当保守的。。
当您同时使用std::vector::reserve
时, std::vector::push_back
效果最好。如果您能在进入紧循环之前使大部分内存可用,那么您就赢了。你甚至不需要知道具体是多少,只要猜就可以了。
你实际上也可以用它胜过std::vector::resize
的性能,因为std::vector::resize
使用alloc
,而std::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.Add
和vector::push_back
都会随着容器的增长不时地重新分配内存。c++ vector按值存储子向量,因此它们的所有数据(在您的情况下似乎是巨大的)都被一次又一次地复制。相比之下,c# list通过引用存储子列表,因此在重新分配时不会复制子列表的数据。
典型的矢量实现在重新分配时将其容量加倍。所以如果你有100万行,子向量将被复制log(2,1000000)≈10次。
c++ 11中引入的移动语义应该可以消除这种影响。在此之前,请尝试vector< shared_ptr< vector<int> > >
、list< vector<int> >
,或者,如果您提前知道未来的大小,请使用vector::reserve()
来避免重新分配。
还没有测试的代码,但多少ints
通常加载?考虑当每个vectors
达到capacity
时会发生什么。vector
生长效率低下- 0 (n)我相信。c#的List
没有这种行为。
考虑使用std::deque
、std::list
或其他生长性能更好的容器。有关更多信息,请参阅本文。
如果您有非常多的元素,那么每次vector被推回时都需要重新分配和复制。尝试在c++中使用不同的容器。
既然你的函数本身并不慢1,那么程序变慢的原因一定是当pathLookupVectors
被填充时,使用这个函数的乘积的一些代码变慢了。
我认为在你的程序上运行一个分析器是最好的方法,但是你也可以检查你的代码,找到每一块依赖于pathLookupVectors
的代码,并考虑它是否可能是你正在寻找的瓶颈。
<子> 1。子>