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

时间:2011-10-18 15:06:52

标签: c# c++ io

我已经将这个简单的方法从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实例并在完成的同时运行单个方法程序很快。我意识到问题可能是一些基本的设置,但我没有经历过这样的道歉,如果这确实令人失望地成为原因。我仍然不知道为什么花了这么长时间。

9 个答案:

答案 0 :(得分:9)

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

39%的时间花在basic_istream :: operator&gt;&gt;上

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%的其他功能

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

答案 1 :(得分:7)

代码中的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));
 }

完成!

答案 2 :(得分:7)

根据您的更新,很明显您自己发布的功能不会导致性能问题,因此虽然您可以通过多种方式对其进行优化,但这似乎无济于事。

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

  • 如果您正在以调试模式编译程序(即没有优化),则重新编译以进行发布(完全优化,支持速度),看看是否有所作为。

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

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

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

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

  • 仅对要调试的文件禁用优化(确保优化至少对具有慢速功能的文件保持启用状态)。要禁用文件的优化,请在Solution Explorer中选择该文件,右键单击,选择Properties,然后转到Configuration Properties | C / C ++ / Optimization。查看该页面中的所有项目是如何为Debug构建设置的,并复制Release版本中的所有项目。对您希望调试器可用的所有文件重复此操作。

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

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

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

我希望这会有所帮助。

答案 3 :(得分:4)

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


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

void Level::PopulatePathVectors(const string &pathTable)

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


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

tempString.append(line[count]);

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

您可以执行此操作,例如使用tempString

答案 4 :(得分:4)

以下是我没见过的其他人提及的一些事情。它们有点含糊不清,但由于无法重现事物,因此很难详细说明所有内容。

穷人的分析。

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

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

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

<强>缓冲

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

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

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

<强>预订。

当您还使用std::vector::push_back时,

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

您也可以通过此方式获得std::vector::resize的效果,因为std::vector::resize使用allocstd::vector::push_back将使用realloc

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

然而,如果你使用reserve,push_back可以更快地运行。

字符串拆分。

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

以下是我通常使用的参考资料。

Split array of chars into two arrays of chars

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

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

STL shenanigans。

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

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

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

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

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

答案 5 :(得分:3)

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

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

C ++ 11中引入的

移动语义应该可以消除这种影响。在此之前,请尝试vector< shared_ptr< vector<int> > >list< vector<int> >,或者,如果您事先了解未来的尺寸,请使用vector::reserve()以避免重新分配。

答案 6 :(得分:1)

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

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

答案 7 :(得分:1)

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

答案 8 :(得分:1)

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

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

<子> 1。建立在您最新的编辑中。