问题 Scala for循环和迭代器


假设我有一个非常大的可迭代值集合(大约100,000个字符串条目,逐个从磁盘读取),我在其笛卡尔积上做了一些事情(并将结果写回磁盘,但我不会在这里显示):

for(v1 <- values; v2 <- values) yield ((v1, v2), 1)

我知道这只是另一种写作方式

values.flatMap(v1 => values.map(v2 => ((v1, v2), 1)))

这显然导致每个flatMap迭代(甚至整个笛卡尔积?)的整个集合保存在内存中。如果你使用for循环读取第一个版本,这显然是不必要的。理想情况下,只有两个条目(被组合的条目)应始终保存在内存中。

如果我重新制定第一个版本:

for(v1 <- values.iterator; v2 <- values.iterator) yield ((v1, v2), 1)

内存消耗要低很多,这使我认为这个版本必须根本不同。它在第二个版本中的确有何不同?为什么Scala不会隐式使用第一个版本的迭代器?在某些情况下不使用迭代器时是否有任何加速?

谢谢! (还要感谢“lmm”谁回答了这个问题的早期版本)


8083
2017-12-10 15:25


起源

如果你屈服了 ((v1, v2), 1) 你构建一个包含所有这些元组的新集合。所以整个carthesian产品确实必须留在记忆中,不是吗? - Jasper-M
不一定,它们被写回磁盘(使用spark / HDFS)。否则它不会太好扩展:) - Johannes


答案:


第一个版本是严格评估的;它创建了一个包含所有这些值的真实,具体的集合。第二个“只是”提供了一个 Iterator,让你迭代所有的值;它们将在您实际执行迭代时创建。

Scala默认为第一个的主要原因是因为scala作为一种语言允许副作用。如果您将两个映射写为:

for(v1 <- values; v2 <- values) yield {println("hello"); ((v1, v2), 1)}
for(v1 <- values.iterator; v2 <- values.iterator) yield {
  println("hello"); ((v1, v2), 1)}

然后第二个会发生什么可能会让你感到惊讶,特别是在一个更大的应用程序中,迭代器可能会远离它实际使用的位置。

如果映射操作本身很昂贵,并且您创建一次并重复使用多次,则集合将比迭代器执行得更好 - 迭代器每次都必须重新计算值,而集合存在于内存中。可以说这使得集合性能更具可预测性 - 它消耗了大量内存,但无论使用何种集合,它的数量都是相同的。

如果你想要一个更愿意忽视操作和优化的集合库 - 也许是因为你已经编写了所有代码而没有副作用 - 你可能想要考虑 保罗飞利浦的新努力


5
2017-12-10 15:53



因此,虽然理解的第一个可能扩展为“values.flatMap(v1 => values.map(v2 =>((v1,v2),1)))”,使用迭代器扩展到的理解是什么?一样? - Johannes
同样的是,但是 .flatMap 上 Iterator 有一个不同的实现 Array。 - lmm
很高兴知道。谢谢! - Johannes


在斯卡拉, yield 不产生懒惰的序列。我的理解是,您可以一次获取所有值,以便将它们全部索引为集合。例如,我为光线跟踪器编写了以下内容来生成光线:

def viewRays(aa:ViewHelper.AntiAliasGenerator) =
{
  for (y <- 0 until height; x <- 0 until width)
    yield (x, y, aa((x, y)))
}

它失败了(记忆力不足),因为它使所有的光线都在前面(惊喜!)。通过使用 .iterator 方法,你特别要求一个懒惰的迭代器。上面的例子可以修改为:

def viewRays(aa:ViewHelper.AntiAliasGenerator) =
{
  for (y <- 0 until height iterator; x <- 0 until width iterator)
    yield (x, y, aa((x, y)))
}

它以懒惰的方式工作。


7
2017-12-10 15:48



打败我。是的,这完全是!但是,有一条评论告诫他们,在for comprehension中使用它之前显式创建迭代器不会产生他们想要的相同结果。 - wheaties
你的意思不是吗? yield 不 自动 产生一个懒惰的序列?如果它是从懒惰的东西构建的(如你在这里所示)。 - chiastic-security