繁体   English   中英

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

[英]Why is this so much slower in 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<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实例并在完成的同时运行单个方法程序很快。 我意识到问题可能是一些基本的设置,但我没有经历过这样的道歉,如果这确实令人失望地成为原因。 我仍然不知道为什么它需要这么长时间。

我用Very SleepyVisual 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%其他功能

有些东西似乎来自内联调用,因此有点难以说出它实际来自哪里。 但你仍然可以得到这个想法。 这里应该做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<int>( ss ), 
          std::istream_iterator<int>(), 
          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<int> begin(ss), end;
   pathLookupVectors[i].push_back(vector<int>(begin, end));
 }

完成!

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

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

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

  • 要检查是否在此可疑函数上花费了额外时间,可以在函数的开头和结尾添加包含时间戳的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构建中。

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

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

我希望这有帮助。

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


你的弦有多大? 当您在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使用allocstd::vector::push_back将使用realloc

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

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

字符串拆分。

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

这是我通常使用的参考。

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

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

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

STL诡计。

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

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

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

某些容器有奇怪的性能特征。 我不会遇到矢量问题,但我真的不知道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<int> > >list< vector<int> > ,或者,如果您事先知道未来的大小,请使用vector::reserve()来避免重新分配。

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM