不可变是一个记忆猪吗?

cis*_*eat 27 memory functional-programming immutability

假设我们有一个类似于内存的类Image,有可链接的方法,比如Resize()ConvertTo().

如果这个类是不可变的,那么当我开始做类似的事情时,它会不会占用大量的内存i.Resize(500, 800).Rotate(90).ConvertTo(Gif),相比之下,一个可变的自我修改?如何用函数式语言处理这种情况?

Nor*_*sey 24

如果这个类是不可变的,它不会占用大量的内存吗?

通常,您对该单个对象的内存要求可能会翻倍,因为您可能会同时拥有"旧副本"和"新副本".因此,您可以在程序的生命周期中查看此现象,因为分配的大对象比在典型的命令性程序中分配的大一个.(没有"工作"的对象只是坐在那里,具有与任何其他语言相同的内存要求.)

如何用函数式语言处理这种情况?

什么都不做. 或者更准确地说,分配健康状况​​良好的新对象.如果您正在使用为函数式编程设计的实现,那么分配器和垃圾收集器几乎肯定会针对高分配率进行调整,一切都会好的.如果您不幸尝试在JVM上运行功能代码,那么性能将不如定制实现那么好,但对于大多数程序来说,它仍然会很好.


你能提供更多细节吗?

当然.我将采用一个非常简单的例子:1000x1000灰度图像,每像素8位,旋转180度.这就是我们所知道的:

  • 在内存中表示图像需要1MB.

  • 如果图像是可变的,则可以通过进行更新来旋转180度.所需的临时空间量足以容纳一个像素.你编写了一个双重嵌套循环

    for (i in columns) do
      for (j in first half of rows) do {
         pixel temp := a[i, j]; 
         a[i, j] := a[width-i, height-j]; 
         a[width-i, height-j] := tmp
      }
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果图像是不可变的,则需要创建一个完整的新图像,暂时您必须挂起旧图像.代码是这样的:

    new_a = Image.tabulate (width, height) (\ x y -> a[width-x, height-y])
    
    Run Code Online (Sandbox Code Playgroud)

    tabulate函数分配一个完整的,不可变的2D数组并初始化其内容.在此操作期间,旧图像暂时占用内存.但是当tabulate完成时,a不再使用旧图像,并且其内存现在是免费的(也就是说,有资格由垃圾收集器回收).然后,所需的临时空间量足以容纳一个图像.

  • 轮换正在进行时,不需要复制其他类的对象; 只有旋转的图像需要临时空间.

NB对于其他操作,诸如重新缩放或旋转90度(非正方形)图像,这是很可能的是,即使当图像是可变的,则整个图像的临时副本将是必要的,因为尺寸变化.另一方面,可以使用具有非常小的临时空间的突变来逐个像素地完成颜色空间变换和其他计算.

  • 我认为即使是一个具有可变状态的对象,在对大多数实质变换进行变换时都需要源缓冲区和目标缓冲区,特别是像`Resize()`和`Rotate()`这样的东西. (5认同)
  • @gabe:现代垃圾收集器和内存分配器非常容易识别缓存.新对象分配在名为*nursery*的区域中,该区域始终位于同一位置.只复制次要集合中存在的对象,因此不需要"大量复制".托儿所始终位于同一地点并且是连续的,而不是零散的,因此分配非常快.这些都不是火箭科学; 这些收集器自1995年以来已经部署并且自2000年以来广泛使用.一些JVM已经迟到了,但解决方案是众所周知的. (3认同)

Dan*_*ory 11

是.不可变性是计算中永恒时空权衡的一个组成部分:您牺牲内存来换取上述锁和其他并发访问控制措施所带来的并行性提高的处理速度.

功能语言通常通过将它们分成非常精细的颗粒来处理这种性质的操作.您的Image类实际上并不保存图像的逻辑数据位; 相反,它使用指针或引用包含图像数据的更小的不可变数据段.当操作需要对图像数据进行的,较小的片段克隆和突变,和图像的新副本更新为引用返回 - 这点尚未被复制或改变,一直保持完整的数据大部分.

这就是功能设计需要与命令式设计不同的基本思维过程的一个原因.算法本身不仅布局不同,而且数据存储和结构也需要以不同的方式布局,以解决复制的内存开销问题.