简单循环太慢
Simple loop is too slow
我不明白为什么下面的代码太慢了。这段代码的目标非常简单:我有一组点,我想将它们分成 6 个桶(因此每个桶 100000 个点)。代码:
import scala.collection.mutable.{Map, ListBuffer}
object Main {
def main(args : Array[String]) = {
val m : Map[String, ListBuffer[Double]] = Map()
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(600000){0.0}
var it = 0
val t1 = System.currentTimeMillis
for (i <- 0 until points.length) {
if(it == labels.length-1) it = 0
val point = points(i)
val currentLabel = labels(it)
val values = m.getOrElse(currentLabel, ListBuffer())
m += (currentLabel -> (values :+ point))
it += 1
println("it -> = " + it)
}
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
}
}
访问列表缓冲区上的映射和追加需要固定时间,所以对我来说,这段代码的复杂度为 O(n),其中 n 是要拆分的点数。我可以提出一些建议来使这段代码更快吗?
正如@Noah 在评论中正确注意到的那样,您不必将缓冲区推回地图。这应该足够了:
val values = m.getOrElseUpdate(currentLabel, ListBuffer())
values += point
或者您可以使用函数式方法来完成,如果您使用 Scala,建议这样做:
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(60000){2.0}
val t1 = System.currentTimeMillis
val m = points.zipWithIndex.groupBy {
case (point, i) => labels(i % labels.size)
}.mapValues(arr => arr.map(_._1).toList)
val t2 = System.currentTimeMillis
println(m)
println("fill values in = " + (t2-t1) + " msecs")
请注意 - 这里没有可变数据结构
以下重构不会导致创建与点一样多的集合,并且依赖于 Scala API、
object Main {
def main(args : Array[String]) = {
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(600000){0.0}
val t1 = System.currentTimeMillis
val xst = points.grouped(labels.size).toArray.transpose
val m = (labels zip xst).toMap
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
}
}
虽然原始代码需要几分钟,但这个代码需要大约 700 毫秒。
此代码避免了索引引用和更新现有集合。
用我填满内存的代码更新(Alifirat)
object Main {
def main(args : Array[String]) = {
val labels = Array("1","2","3","4","5","6", "7")
val points = Array.fill(7000000){0.0}
val t1 = System.currentTimeMillis
val xst = points.grouped(labels.size).toArray.transpose
val m = (labels zip xst).toMap
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
}
}
相同的代码,但 运行 7 个桶的 7 000 000 个点。
更新
尝试
scala -J-Xmx4g
然后粘贴更新后的代码。
更新
如果最终映射映射到 0.0
的数组上,以下证明在 7000 万个点上速度相当快,
val m = labels.map(l => l -> Array.fill(10*1000*1000){0.0}).toMap
如果性能是必不可少的,我已经建议的面向 C 的方法证明了内存和时间效率,可能以可伸缩性和组合性为代价。
给猫剥皮的另一种方法:
val buckets = Array.fill(labels.length)(ArrayBuffer.empty[Double])
points.zipWithIndex.foreach{case(p, i) => buckets(i%labels.length) += p}
(labels zip buckets).toMap
我没有正确地对其进行基准测试,但这是我尝试过的最快的方法(不再是 - 请参阅我的其他答案)
有时正确的方法是改变函数式风格。
我用 c 风格的 Scala 编写了一个解决方案。
def main(args : Array[String]) = {
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(600000){0.0}
val numChannels=6;
val channels=new Array[Array[Double]](numChannels);
for(i<-0 until numChannels){
channels(i)=new Array[Double]((points.length/6));
}
var from=0;
val t1 = System.currentTimeMillis
for(i<-0 until numChannels){
val channel=channels(i);
from=i
var to=0;
try{
while(true){
channel(to)=points(from);
to=to+1;
from=from+numChannels;
}
} catch {
case e:ArrayIndexOutOfBoundsException =>{
//Ok finished this channel
}
}
}
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
println("from:"+from)
}
此解决方案仅完成需要完成的工作,仅此而已。
在我的机器上它使用 8 毫秒。
Elm 的代码使用 309 毫秒。
原来用了 221226 毫秒。
虽然我喜欢 Scala,但重要的是要记住它擅长隐藏其操作的计算成本。大多数对集合的操作都会为每个元素引入至少几个过程调用。
这里(作为一个单独的答案,因为它与我的第一个答案完全不同)是另一个 "C style",它确实处理可变数量的桶。
有趣的是,在我的机器上,它的速度大约是 Espen Brekke 的其他 C 风格答案的两倍。更新:我收回这一点——两者都运行太快了,实际执行时间被启动时间等掩盖了。运行在100倍的点数(60000000)上,Espen的速度大约是两倍(但是有固定数量的桶) - 大约 220 毫秒与大约 420 毫秒相比
更新 2:Espen 的最新版本现在或多或少与下面相同(除了使用异常而不是显式边界检查),并且毫不奇怪,运行速度相似。
另请注意,这两个版本都没有计算存储桶数组的创建时间。在我的机器上,这大约是另外 200 毫秒(对于任一版本),所以大约 50%-100% 的时间将东西放入桶中...
因此,这个变体比 OP 的原始代码(在我的机器上)快大约 100,000 倍!
object buckets2 extends App {
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(600000){0.0}
val count = 6
val bucket_size = ((points.length-1)/count) + 1
val buckets = Array.fill(count) (new Array[Double](bucket_size));
var b = 0
val t1 = System.currentTimeMillis
while (b < count) {
var i = b
var j = 0
val this_bucket = buckets(b)
while (i < points.length) {
this_bucket(j) = points(i)
i = i + count
j = j + 1
}
b = b + 1
}
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
}
我不明白为什么下面的代码太慢了。这段代码的目标非常简单:我有一组点,我想将它们分成 6 个桶(因此每个桶 100000 个点)。代码:
import scala.collection.mutable.{Map, ListBuffer}
object Main {
def main(args : Array[String]) = {
val m : Map[String, ListBuffer[Double]] = Map()
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(600000){0.0}
var it = 0
val t1 = System.currentTimeMillis
for (i <- 0 until points.length) {
if(it == labels.length-1) it = 0
val point = points(i)
val currentLabel = labels(it)
val values = m.getOrElse(currentLabel, ListBuffer())
m += (currentLabel -> (values :+ point))
it += 1
println("it -> = " + it)
}
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
}
}
访问列表缓冲区上的映射和追加需要固定时间,所以对我来说,这段代码的复杂度为 O(n),其中 n 是要拆分的点数。我可以提出一些建议来使这段代码更快吗?
正如@Noah 在评论中正确注意到的那样,您不必将缓冲区推回地图。这应该足够了:
val values = m.getOrElseUpdate(currentLabel, ListBuffer())
values += point
或者您可以使用函数式方法来完成,如果您使用 Scala,建议这样做:
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(60000){2.0}
val t1 = System.currentTimeMillis
val m = points.zipWithIndex.groupBy {
case (point, i) => labels(i % labels.size)
}.mapValues(arr => arr.map(_._1).toList)
val t2 = System.currentTimeMillis
println(m)
println("fill values in = " + (t2-t1) + " msecs")
请注意 - 这里没有可变数据结构
以下重构不会导致创建与点一样多的集合,并且依赖于 Scala API、
object Main {
def main(args : Array[String]) = {
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(600000){0.0}
val t1 = System.currentTimeMillis
val xst = points.grouped(labels.size).toArray.transpose
val m = (labels zip xst).toMap
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
}
}
虽然原始代码需要几分钟,但这个代码需要大约 700 毫秒。
此代码避免了索引引用和更新现有集合。
用我填满内存的代码更新(Alifirat)
object Main {
def main(args : Array[String]) = {
val labels = Array("1","2","3","4","5","6", "7")
val points = Array.fill(7000000){0.0}
val t1 = System.currentTimeMillis
val xst = points.grouped(labels.size).toArray.transpose
val m = (labels zip xst).toMap
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
}
}
相同的代码,但 运行 7 个桶的 7 000 000 个点。
更新
尝试
scala -J-Xmx4g
然后粘贴更新后的代码。
更新
如果最终映射映射到 0.0
的数组上,以下证明在 7000 万个点上速度相当快,
val m = labels.map(l => l -> Array.fill(10*1000*1000){0.0}).toMap
如果性能是必不可少的,我已经建议的面向 C 的方法证明了内存和时间效率,可能以可伸缩性和组合性为代价。
给猫剥皮的另一种方法:
val buckets = Array.fill(labels.length)(ArrayBuffer.empty[Double])
points.zipWithIndex.foreach{case(p, i) => buckets(i%labels.length) += p}
(labels zip buckets).toMap
我没有正确地对其进行基准测试,但这是我尝试过的最快的方法(不再是 - 请参阅我的其他答案)
有时正确的方法是改变函数式风格。 我用 c 风格的 Scala 编写了一个解决方案。
def main(args : Array[String]) = {
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(600000){0.0}
val numChannels=6;
val channels=new Array[Array[Double]](numChannels);
for(i<-0 until numChannels){
channels(i)=new Array[Double]((points.length/6));
}
var from=0;
val t1 = System.currentTimeMillis
for(i<-0 until numChannels){
val channel=channels(i);
from=i
var to=0;
try{
while(true){
channel(to)=points(from);
to=to+1;
from=from+numChannels;
}
} catch {
case e:ArrayIndexOutOfBoundsException =>{
//Ok finished this channel
}
}
}
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
println("from:"+from)
}
此解决方案仅完成需要完成的工作,仅此而已。
在我的机器上它使用 8 毫秒。
Elm 的代码使用 309 毫秒。
原来用了 221226 毫秒。
虽然我喜欢 Scala,但重要的是要记住它擅长隐藏其操作的计算成本。大多数对集合的操作都会为每个元素引入至少几个过程调用。
这里(作为一个单独的答案,因为它与我的第一个答案完全不同)是另一个 "C style",它确实处理可变数量的桶。
有趣的是,在我的机器上,它的速度大约是 Espen Brekke 的其他 C 风格答案的两倍。更新:我收回这一点——两者都运行太快了,实际执行时间被启动时间等掩盖了。运行在100倍的点数(60000000)上,Espen的速度大约是两倍(但是有固定数量的桶) - 大约 220 毫秒与大约 420 毫秒相比
更新 2:Espen 的最新版本现在或多或少与下面相同(除了使用异常而不是显式边界检查),并且毫不奇怪,运行速度相似。
另请注意,这两个版本都没有计算存储桶数组的创建时间。在我的机器上,这大约是另外 200 毫秒(对于任一版本),所以大约 50%-100% 的时间将东西放入桶中...
因此,这个变体比 OP 的原始代码(在我的机器上)快大约 100,000 倍!
object buckets2 extends App {
val labels = Array("1","2","3","4","5","6")
val points = Array.fill(600000){0.0}
val count = 6
val bucket_size = ((points.length-1)/count) + 1
val buckets = Array.fill(count) (new Array[Double](bucket_size));
var b = 0
val t1 = System.currentTimeMillis
while (b < count) {
var i = b
var j = 0
val this_bucket = buckets(b)
while (i < points.length) {
this_bucket(j) = points(i)
i = i + count
j = j + 1
}
b = b + 1
}
val t2 = System.currentTimeMillis
println("fill values in = " + (t2-t1) + " msecs")
}