什么时候可以并行调用使用可变变量的函数?
When can I call a function using mutable variables in parallel?
看过 Phil Trelford
的有趣讲座后
https://www.youtube.com/watch?v=hx2vOwbB-X0
我对通过用数组替换列表以及更普遍地使用可变变量来加速我的代码的可能性很感兴趣。所以我做了一个简单的测试:
let xs = [0.0..1000000.00]
let axs = List.toArray xs
let f x = sin (x * x)
List.map f xs // Real: 00:00:00.170, CPU: 00:00:00.187, GC gen0: 5, gen1: 3, gen2: 1
Array.map f axs // Real: 00:00:00.046, CPU: 00:00:00.046, GC gen0: 0, gen1: 0, gen2: 0
通过数组映射比通过列表映射快三倍多。在这一点上,我还没有测试调用的函数计算量更大时的速度差异。差异可能只是因为在数组中的项目中移动速度更快,并且当每次迭代计算密集时可能变得微不足道。
不过,在某些情况下,使用数组或更普遍的可变变量可能会产生显着差异。
在更改我的代码以使用数组而不是列表之前,我想更清楚地了解代码并行化时的后果。
一般来说,什么时候可以使用可变变量而不冒并行代码出现问题的风险?是否有一个简单的测试可以让我确定函数在并行调用时的稳健性?
与数组的速度差异与可变性无关;都是关于 cache locality。数组在内存中是连续的,因此它们比列表更快地迭代:F# 列表是单链表,因此每个项目都可以(并且通常)位于不同的内存位置。这意味着您不会从 CPU 的缓存中获益,而对于数组,一旦您支付了从内存中检索第一项的成本,然后是第二项到第 N 项(其中 N 的值取决于您要检索的项目的大小)已经在缓存中并准备好进行近乎即时的检索。如果 F# 有一个 ImmutableArray class 并且您使用了它,那么在通过该 ImmutableArray 进行映射时,您将获得与从您的可变数组进行映射时相同的速度优势。
至于你的主要问题,关于何时可以安全地将可变变量与并行代码一起使用,简单的测试是问 "Am I actually mutating the data that multiple threads are using?" 如果你不改变你的数据,那么拥有多个线程是安全的并行访问它。即使数据 可以 发生变异(例如,一个数组),只要您实际上没有改变它,那么您的并行代码就不会 运行 出现问题。如果您确实改变了数据,那么您必须处理锁定,以及所有伴随锁定而来的问题,例如资源匮乏、死锁等。
所以简单的经验法则是 "Mutating data + Parallelism = Pain"。如果您改变了数据但没有 运行 宁并行代码,那么您的痛苦就会少得多。如果您不改变数据,那么并行代码不会给您带来痛苦。但是,如果您两者都做,请做好头痛的准备。
虽然@rmunn 对实际问题提供了很好的答案,但我觉得我必须写这个附录,因为我认为它很重要,而且它太长了,不适合发表评论。
这是为了回答 隐含的 问题,我读作“既然可变数据更快,我不应该总是使用可变数据吗?"
确实如此,一般来说,可变数据结构,如果你做对了,表面上速度更快,那么我们为什么不一直使用它们呢?如果你做对了,跳转也确实比函数调用更快,所以为什么 don't we use goto
all the time?手动内存管理也确实如此,如果你做对了,使用的内存更少,和比垃圾收集更快,那么我们为什么要使用垃圾收集呢?并且(可以说)直接用汇编甚至二进制代码编写是真的,如果你得到它 真的,真的 对,比编译更快,那么为什么我们有高级有语言吗?
以上所有问题的答案是,性能并不是软件开发中唯一关心的问题。它甚至不是最重要的问题。甚至可以争辩说,它不在任何地方 接近 最重要的关注点。在现代更重要的是可读性、稳定性、可维护性、整体弹性。
在设计系统时,首先尝试猜测可能出现瓶颈的地方,然后仔细设计这些地方并在它们周围放置一些日志记录和检测。在编写程序时,首先要使它们可读、可理解、可维护。然后测量性能 - 在生产环境中,或者如果您负担得起的话,在临时环境中。我说的 "measure" 不是 "is it the fastest it can be?",而是 "is it fast enough for our purposes?"。如果是,那很好。如果不是,找出减速的确切位置,并优化那个地方。随着时间的推移,随着经验的积累,您对潜在瓶颈的猜测会越来越好。
不要尝试提前优化:您最终只会弄得手上一团糟,而且您必须尽快将其扔掉。 Premature optimization is the root of all evil.
看过 Phil Trelford
https://www.youtube.com/watch?v=hx2vOwbB-X0
我对通过用数组替换列表以及更普遍地使用可变变量来加速我的代码的可能性很感兴趣。所以我做了一个简单的测试:
let xs = [0.0..1000000.00]
let axs = List.toArray xs
let f x = sin (x * x)
List.map f xs // Real: 00:00:00.170, CPU: 00:00:00.187, GC gen0: 5, gen1: 3, gen2: 1
Array.map f axs // Real: 00:00:00.046, CPU: 00:00:00.046, GC gen0: 0, gen1: 0, gen2: 0
通过数组映射比通过列表映射快三倍多。在这一点上,我还没有测试调用的函数计算量更大时的速度差异。差异可能只是因为在数组中的项目中移动速度更快,并且当每次迭代计算密集时可能变得微不足道。
不过,在某些情况下,使用数组或更普遍的可变变量可能会产生显着差异。
在更改我的代码以使用数组而不是列表之前,我想更清楚地了解代码并行化时的后果。
一般来说,什么时候可以使用可变变量而不冒并行代码出现问题的风险?是否有一个简单的测试可以让我确定函数在并行调用时的稳健性?
与数组的速度差异与可变性无关;都是关于 cache locality。数组在内存中是连续的,因此它们比列表更快地迭代:F# 列表是单链表,因此每个项目都可以(并且通常)位于不同的内存位置。这意味着您不会从 CPU 的缓存中获益,而对于数组,一旦您支付了从内存中检索第一项的成本,然后是第二项到第 N 项(其中 N 的值取决于您要检索的项目的大小)已经在缓存中并准备好进行近乎即时的检索。如果 F# 有一个 ImmutableArray class 并且您使用了它,那么在通过该 ImmutableArray 进行映射时,您将获得与从您的可变数组进行映射时相同的速度优势。
至于你的主要问题,关于何时可以安全地将可变变量与并行代码一起使用,简单的测试是问 "Am I actually mutating the data that multiple threads are using?" 如果你不改变你的数据,那么拥有多个线程是安全的并行访问它。即使数据 可以 发生变异(例如,一个数组),只要您实际上没有改变它,那么您的并行代码就不会 运行 出现问题。如果您确实改变了数据,那么您必须处理锁定,以及所有伴随锁定而来的问题,例如资源匮乏、死锁等。
所以简单的经验法则是 "Mutating data + Parallelism = Pain"。如果您改变了数据但没有 运行 宁并行代码,那么您的痛苦就会少得多。如果您不改变数据,那么并行代码不会给您带来痛苦。但是,如果您两者都做,请做好头痛的准备。
虽然@rmunn 对实际问题提供了很好的答案,但我觉得我必须写这个附录,因为我认为它很重要,而且它太长了,不适合发表评论。
这是为了回答 隐含的 问题,我读作“既然可变数据更快,我不应该总是使用可变数据吗?"
确实如此,一般来说,可变数据结构,如果你做对了,表面上速度更快,那么我们为什么不一直使用它们呢?如果你做对了,跳转也确实比函数调用更快,所以为什么 don't we use goto
all the time?手动内存管理也确实如此,如果你做对了,使用的内存更少,和比垃圾收集更快,那么我们为什么要使用垃圾收集呢?并且(可以说)直接用汇编甚至二进制代码编写是真的,如果你得到它 真的,真的 对,比编译更快,那么为什么我们有高级有语言吗?
以上所有问题的答案是,性能并不是软件开发中唯一关心的问题。它甚至不是最重要的问题。甚至可以争辩说,它不在任何地方 接近 最重要的关注点。在现代更重要的是可读性、稳定性、可维护性、整体弹性。
在设计系统时,首先尝试猜测可能出现瓶颈的地方,然后仔细设计这些地方并在它们周围放置一些日志记录和检测。在编写程序时,首先要使它们可读、可理解、可维护。然后测量性能 - 在生产环境中,或者如果您负担得起的话,在临时环境中。我说的 "measure" 不是 "is it the fastest it can be?",而是 "is it fast enough for our purposes?"。如果是,那很好。如果不是,找出减速的确切位置,并优化那个地方。随着时间的推移,随着经验的积累,您对潜在瓶颈的猜测会越来越好。
不要尝试提前优化:您最终只会弄得手上一团糟,而且您必须尽快将其扔掉。 Premature optimization is the root of all evil.