【问题标题】:how to remove nested foreach loops for performance emprovement如何删除嵌套的 foreach 循环以提高性能
【发布时间】:2023-03-09 17:15:01
【问题描述】:

我有一个基于性能的问题。 有没有办法删除嵌套的 foreach 循环,用更高效的东西代替它们?这是一个例子:

List<foo> foos = SelectAllfoos();

foreach(foo f in foos){
    //dosomething

    foreach(foo2 f2 in foo.GetFoos2()){
        //dosomething
    }

    foreach(foo3 f3 in foo.GetFoos3()){
        //dosomething
    }

    foreach(foo4 f4 in foo.GetFoos4()){
        //dosomething

        foreach(foo4_1 f4_1 in f4.GetFoos4_1()){
            //dosomething
        }
    }
}

Obiouvsly 这是我刚刚为这个例子发明的假代码。但是想象一下你有类似的东西。您应该如何提高此方法的性能?

PS:我已经尝试过使用System.Threading.Task.Parallel.ForEach,它可以提高性能,但我的意思是编写此代码的更好方法。

PPS:这是用 C# 编写的,但我的问题涉及更广泛的范围,适用于所有语言。

【问题讨论】:

  • 1.这真的取决于这些somethings 是什么。 2.值得运行一个分析器来找出最慢的部分,这些嵌套循环可能根本不是瓶颈。 3. 并行听起来不错,但是请记住,如果它是服务器应用程序,它可能会减慢其他用户的响应时间,而对于客户端应用程序,它可能会使其他进程(甚至整个操作系统)无响应。一般来说,我不鼓励使用并行性作为低性能的解决方案。
  • 我建议使用专用线程查找上述实现
  • 有些明显的非答案是“尽量避免首先设计具有多级嵌套的算法”。但是,当您有一个虚构的示例时,很难直接回答“是否有更好的方法来编写此代码”。例如。如果您在 3D 中迭代某些东西,存储所有感兴趣对象的 1D 列表可能会更简单,每个对象都有一个位置。这样,您就不必遍历空白区域。
  • 显然,这不适用于您必须进行嵌套(因此“未回答”),但通常当我被问到这个问题时,通常是因为程序员已经嵌套不恰当(我看到的记录是一个9级深的字符串解析器)。
  • 一个循环不会因为嵌入另一个循环而变慢。它变成的东西被使用了更多次。

标签: performance foreach nested-loops


【解决方案1】:

由于这个问题相当笼统,并且只关注循环,而这些循环没有提供有关实际工作的信息,所以我只能提供一个一般性的答案。

您通常最不想关注的是循环机制本身。这些通常会产生很小的影响(如果有的话)。

通常,如果您遇到这种算法改进已失效的情况(例如:顺序循环不能比线性时间复杂度更好,因为它们无论如何都需要遍历和对每个元素执行某些操作),那么最大的两个改进通常来自并行化和内存优化。

不幸的是,后者很少被讨论,尤其是在高级语言中,但通常会产生同样或更多的影响。它可以将执行时间提高几个数量级,并且适用于任何语言。缓存效率等概念不是依赖于语言的概念,因为无论我们使用哪种编程语言,硬件都保持不变(尽管不同语言的实现方式可能会有很大差异)。

内存访问模式

以图像处理算法为例。在这种情况下,给定两个相同的机器指令(除了它们被交换的事实),在外循环中一次访问一个水平扫描线的像素的内存访问模式可以显着优于访问垂直列像素的内存访问模式像素数。即使对于具有相同总指令级成本(尽管指令成本是可变的)但只是以交换顺序访问内存的其他相同机器指令也是如此。

这是因为,粗略地说,计算机将数据从较慢形式的内存中以连续的块(页面、缓存行)形式获取到较快的内存形式中。当您水平访问图像的像素时,相邻的水平像素块可能会从较慢形式的内存中获取到较快的形式,并且您最终会在继续访问之前从较快的内存形式访问所有相邻像素下一系列像素。当您以垂直方式访问图像的像素时,您最终会将水平相邻像素加载到更快的内存形式中,仅使用该列中的一个像素。由于缓存未命中,结果可能会显着减慢生成的图像算法,因为在将其加载到更小但更快的内存形式之前,我们无法使用所有可用数据(我们基本上是在浪费更小但更快的内存带来了很多好处)。

因此,通常如果您想让循环运行得更快,并且算法改进已经完成,您需要分析访问内存的方式,甚至可能改变所涉及数据结构的内存布局。当您访问内存中靠近的连续数据时,计算机会喜欢它,而当您以遍布各处的混乱方式访问内存时,计算机就不喜欢它。他们更喜欢将内存内容紧密地打包在一起的数组,而不是将内存分散在各处的链接结构(除非链接结构或其内存分配器经过精心设计,不会那样做)。快速循环并不像循环所做的那样来自改变循环的机制,而是比算法改进甚至并行化更深层次的是那些来自面向数据的设计思维方式的与内存相关的优化。在 C# 等语言中,从数据结构中获得更好的引用局部性的技术之一是对象池。

循环平铺/阻塞

有时您可以通过简单地更改循环数据的方式而不实际更改数据的表示方式来改进内存访问模式。一个这样的例子是循环平铺(又名循环阻塞):https://software.intel.com/en-us/articles/how-to-use-loop-blocking-to-optimize-memory-use-on-32-bit-intel-architecture。但同样,这里的加速并不是来自优化你编写循环的方式本身,而是优化你以利用引用局部性的方式遍历数据的方式。它仍然完全是关于内存访问。

分析

所有这些微观级别的优化技术都倾向于使您的代码更难维护,因此在您手头有大量分析测量的情况下,它们几乎总是最好在事后应用。一般来说,学习优化的第一件事是如何测量,根据硬数据而不是直觉来做。初学者倾向于更多地而不是更少地进行优化,因为他们是基于对可能效率低下的猜测而不是硬数据和适当的测量来进行优化的。对于明显的算法瓶颈,这样做很容易,但其他任何事情通常都需要您手中的分析器。一个好的优化器是一个调度热点的狙击手,而不是一个掷弹兵盲目地向任何可能减慢速度的东西投掷手榴弹。事实上,知道如何正确确定优化的优先级并进行适当的测量可能比了解机器的内部工作原理更重要。因此,可能除了所有这些东西之外,如果您想让循环运行得更快,请首先使用分析器并学习如何正确测量低效率。首先要问的不是如何让事情变得更快,而是实际上需要更快(同样重要的是,如果不是更多,什么不是)。

【讨论】:

  • 这是一个非常完整的答案。非常感谢,因为我是初学者,我不知道你告诉的很多点,我会得到通知并应用它们。非常感谢你,我很感激:)
猜你喜欢
  • 2013-01-29
  • 1970-01-01
  • 2021-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多