代码之家  ›  专栏  ›  技术社区  ›  lothar

好的多线程设计是否过早优化?

  •  10
  • lothar  · 技术社区  · 6 年前

    虽然我喜欢多核系统设计带来的智力挑战,但我意识到其中大部分都是不必要的过早优化。

    但另一方面,通常所有系统都有一些性能需求,稍后将其重构为多线程安全操作是困难的,甚至在经济上是不可能的,因为这将是另一种算法的完全重写。

    在优化和完成任务之间保持平衡的方法是什么?

    10 回复  |  直到 7 年前
        1
  •  1
  •   Mike Jr    15 年前

    我相信线程也遵循优化的规律。
    也就是说,不要浪费时间使快速操作并行进行。
    相反,将线程应用于执行时间较长的任务。

    当然,如果系统开始有1000多个核心,那么这个答案可能会过时,需要修改。但是,再次强调,如果你要“完成事情”,那么你肯定想在那之前发货。

        2
  •  9
  •   S.Lott    15 年前

    如果你跟着 管道 地图缩小 设计模式,这就足够了。
    分解事物使你 能够 在操作系统级多处理管道中运行。

    然后,您可以在实际管道中运行。没有额外的工作。操作系统处理一切。巨大的加速机会。

    您也可以切换到线程。做点小工作。操作系统处理其中的一些部分,线程库处理其余部分。但是,由于您在设计时考虑“流程”,所以您的线程没有任何令人困惑的数据共享问题。想一想就大获全胜。

        3
  •  4
  •   user177800    15 年前

    引入线程不会自动提高性能。

        4
  •  3
  •   DigitalZebra    15 年前

    如果你在做一些复杂的多线程的事情,你最好事先考虑一下/设计好它。否则,您的程序将要么是一个完全的灾难,要么会完美地工作。 大多数 做一些疯狂的事情。用多线程设计一个可以证明是正确的东西是很困难的,但是它非常重要。所以不,我不认为好的多线程设计是过早的优化。

        5
  •  2
  •   Will    15 年前

    他们说几天的编码可以节省设计时间。

    并非所有的问题和框架都是多线程的。例如,您所依赖的库可能不是线程安全的。许多过程是自然连续的,不能分解成可并行的部分。

    多线程/多处理只是并行化的一种方法。例如,您还可以使用异步IO。

    在我的经验中,从单个线程进行异步要比从多线程进行异步明智得多。但是,然后,我编写的程序解决了不同的问题,好吧,几乎所有其他人。

        6
  •  1
  •   helios    15 年前

    也许设计系统是件好事 有一些特点 所以如果你想引入多线程 你可以做得很优雅 .

    我不确定这些特征是什么,但我想到了一个类似的例子:缩放。如果您设计的是无状态系统可以执行的小型操作,那么您将能够更自然地进行扩展。

    在我看来,这类事情似乎很重要。

    如果它是为多线程设计的…那么,过早的做法很重要。

    如果只是确保某些特性允许将来扩展或多线程:那么它就不那么重要了:)

    编辑 哎呀,我又读了一遍:过早优化?

    优化:我不认为这是好事,直到你有系统工作(没有来自尝试优化事物的恶习)。做一个干净的设计,这是最灵活和简单的。接下来,您可以在看到真正需要什么时进行优化。

        7
  •  1
  •   David Gladfelter    15 年前

    我永远不会考虑在应用程序中设计多线程,纯粹是出于推测性的性能考虑。这是因为有了一些适合任何应用程序的技术,很容易在以后进行多线程操作。我想到的技术是:

    • 硬合同
      • 在C++中,可以将方法标记为const,这意味着它不会改变实例变量的值。还可以将方法的输入参数标记为const,这意味着只能对该参数调用const方法。有了这两种技术(并且不使用“技巧”来绕过编译器的强制要求),您可以减少需要多线程感知的操作。
    • 依赖倒置
      • 这是一种通用技术,在构造/初始化时或作为特定方法的方法签名的一部分将对象所需的任何外部对象传递给它。使用这种技术,可以100%清楚地知道哪些对象可能会被一个操作(非常量实例变量加上操作的非常量参数)更改,因为您知道操作的非功能方面的范围,并且可以向可以在并行操作之间共享的对象添加互斥锁等。然后,您可以将并行性设计为正确和高效的。
    • 功能性优于程序性
      • 讽刺的是,这意味着,不要过早地优化。使值对象不可变。例如,在C中,字符串是不可变的,这意味着对它们的任何操作都会返回字符串对象的新实例,而不是现有字符串的修改实例。唯一不应该是不可变的对象是无边界数组或包含无边界数组的对象(如果这些数组可能经常被修改)。我认为不变的物体更容易理解。许多程序员都被教授了程序技术,所以这对我们来说有些陌生,但是当你开始用不变的术语思考时,硬膜前编程的可怕方面,如操作依赖顺序和副作用就消失了。在多线程编程中,这些方面甚至更糟糕,因此在类设计中使用功能样式在很多方面都有帮助。随着机器的快速增长,不可变对象的更高成本变得更容易和更容易证明。今天,这是一个平衡。
        8
  •  1
  •   Mike Dunlavey    15 年前

    线程的存在使多个代理的使用更容易编程。

    • 如果代理是用户,就像每个用户都有一个线程一样,它们使编写程序变得更容易。这不是一个性能问题,而是一个易于编写的问题。

    • 如果代理是I/O设备,则可以很容易地编写并行I/O的程序。这可能是为了表现,也可能不是。

    • 如果代理是CPU核心,它们可以很容易地编写使多个核心并行启动的程序。这就是线程与性能相关的时候。

    换句话说,如果您认为线程==parallelism==performance,那么这只会影响线程的一种用法。

        9
  •  1
  •   Einstein    15 年前

    有三种基本设计选择:同步、异步或同步+多线程。选择一个或多个,如果你是一个疯狂的天才。

    是的,您需要在应用程序的设计阶段了解客户可接受的性能期望,以便能够提前做出正确的选择。对于任何一个不平凡的项目来说,将高级别的性能需求作为事后考虑是非常危险和耗时的。

    如果同步不满足客户要求:

    CPU受限的系统需要选择多线程/进程

    IO限制的系统(最常见)通常可以是异步的,也可以是mt。

    对于IO,有限的利用技术(如状态线程)可以让您吃蛋糕。(同步设计/w异步执行)

        10
  •  0
  •   Null    7 年前

    在优化和获取之间保持平衡的方法是什么? 事情做好了吗?

    在实现细节上要放轻松,但要设计出有足够空间优化的设计。现在这是一个棘手的部分,但一旦你习惯了它就没有听起来那么难了。人们发现自己陷入瓶颈设计的一般原因是 太颗粒状 .

    因此,作为一个极端的例子,以一个视频处理应用程序为例,它的设计围绕一个抽象的 IPixel .抽象的存在是为了让软件能够轻松地处理不同像素格式的视频,并且仍然可以编写统一的代码来处理所有像素格式。

    这样的应用程序在中央设计级别的性能上是拧在一起的,如果没有史诗般的体系结构重写,就不太可能在编辑、编码、解码和回放方面提供有竞争力的性能。这是因为它选择了在一个层次上过于细化的抽象。

    通过选择在一个像素级别上进行抽象,这就产生了基于每个像素的动态调度开销。允许虚拟调度、运行时类型信息(反射,例如)等功能的模拟虚拟指针(或语言所使用的任何类型)通常比整个像素本身大,使其内存使用加倍或三倍,并按顺序处理缓存未命中。此外,如果你想事后在许多领域进行多线程的图像处理,你必须重写每一个与之一起工作的地方。 i像素 一次。

    同时,如果软件只是在一个更粗糙的层次上设计抽象,所有这些都可以避免,比如 IImage 避免将单个像素对象暴露到系统的其余部分。图像实际上是 收集 一次可以处理多个像素的操作。现在,与处理像素相关的处理和内存开销将百万像素图像的处理和内存开销减少到1/1000000,此时它变得微不足道。这也给图像操作留下了足够的空间来做一些事情,比如并行处理像素,现在在中央级别进行矢量化,而不需要重写大量的代码,因为客户机代码不是一次单独处理一个像素,而是请求执行整个图像操作。

    虽然这看起来像是一个无需大脑的图像处理,它本质上是一个非常关键的性能领域,但在其他领域有很大的空间来实现这一点。对于经典的继承示例,您不必 Dog 继承 Mammal . 你可以做 Dogs 继承 Mammals .

    因此,回到做事情的时候,我以面向数据的心态开始,第一次尝试时不要获得最高效的缓存友好、令人尴尬的并行、线程安全、支持SIMD的数据表示和最先进的数据结构和算法。否则,我可以花整整一周的时间,用手中的vtune来调整东西,同时观察基准测试的速度越来越快(我喜欢这样做,但在任何地方和前面都这样做绝对没有效率)。我只在其中考虑了足够多的内容,以确定设计时应该使用的适当的粒度级别: “我应该让系统依赖于 这类事情。它甚至不需要那么多的思考。对OOP来说,就像,“系统每帧处理十万只狗吗?是的/不?如果“是”,不要设计中心 接口,不设计中心 IMammal 接口。设计 继承 IMammals 就像我们避免 i像素 如果我们要一次处理数百万个像素,上面类似的图像处理场景中的界面。

    数据的大小也会给你一个打击。如果数据很小,比如64字节或更小,那么很可能它不应该公开一个积累依赖关系的接口,除非它绝对不是性能关键的接口。相反,它应该公开一个集合接口来同时处理其中的许多事情。同时,如果数据很大,比如说4千字节,那么很有可能它几乎没有帮助公开一个集合接口,您可能只是为了方便而设计一个标量接口,一次处理其中的一个。

    多线程是同样的事情。例如,您不希望锁定在一个级别的粒度太小的地方,也不希望访问模式一直影响共享资源。为了线程安全,您还希望能够获取一段代码,并轻松地解释什么线程正在访问什么状态。要做到这一点,您需要一个更粗糙的设计,其内部具有更同质的处理,这样您就可以轻松地控制和解释设计本身实现中的内存访问模式,最小化对共享资源的访问,避免在某个级别上锁定过细,甚至可能避免直接锁定。只要你的设计留有足够的呼吸空间,你就可以在事后获得很多,但关键是要给自己留有呼吸空间。

    一件微小的事情依赖于整个系统中大量不同的事情来进行非均匀的处理,这样就没有空间了。在那里,你可能最终会遇到一个类似赛车的场景,只有10米的道路可以使用。一个庞大的东西,处理一船的青少年东西,它存储同质,留下无限的空间,优化后。