为什么在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实例并同时运行单个方法程序,这在几秒钟内完成。我意识到问题可能出在一些基本设置上,但我没有经验,所以如果这确实是令人失望的最终原因,我很抱歉。我还是不明白为什么要花这么长时间。

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

我使用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.Addvector::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::dequestd::list或其他生长性能更好的容器。有关更多信息,请参阅本文。

如果您有非常多的元素,那么每次vector被推回时都需要重新分配和复制。尝试在c++中使用不同的容器。

既然你的函数本身并不慢1,那么程序变慢的原因一定是当pathLookupVectors被填充时,使用这个函数的乘积的一些代码变慢了。

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

<子> 1。