如何解决为什么标准迭代器比Kotlin中的序列要快?
我是序列的新手,所以我可能做过(或多或少)非常错误的事情,但是我有一个疑问:
我写了两个函数:
fun isPrimeNumber1(number: Int): Boolean {
if (number <= 1) return false
for (divider in 2 .. number / 2) {
if ( number % divider == 0 ) return false
}
return true
}
和
fun isPrimeNumber2(number: Int): Boolean {
if (number <= 1) return false
return !(2 .. number / 2).asSequence().map { it }.any { number % it == 0 }
}
现在,我正在运行用kotest
编写的测试,其中两个函数都将Int.MAX_VALUE
接收为number
。
class MyTestPrime : FunSpec({
context("Prime numbers") {
test("Should return true if number is prime (1st fun)") {
isPrimeNumber1(Int.MAX_VALUE) shouldBe true
}
test("Should return true if number is prime (2nd fun)") {
isPrimeNumber2(Int.MAX_VALUE) shouldBe true
}
}
})
isPrimeNumber1()
函数的执行时间约为3.5秒,而第二函数isPrimeNumber2()
的执行时间约为8.5秒。
为什么会这样?我是否缺少有关序列的信息?还是我的代码以正确但非常不理想的方式达到了目的?
解决方法
这是预期的。具有序列的变体创建一个Iterator
对象,并为每个元素调用.hasNext()
和.next()
函数。
由于Iterator
适用于对象而不是图元,因此所有int
都通过Integer::valueOf
调用进行装箱。 (请注意:.map { it }
步骤是多余的。
我通过IntelliJ Idea中的Java Flight Recorder运行了这两个功能,与其他变体相比,序列变体引起了更多的函数调用。
isPrimeNumber1:
isPrimeNumber2:
如您所见,isPrimeNumber2
变体导致更多函数在后台被调用,因此受到其开销的影响。
检查它的另一种方法是将两个函数的字节码反编译为Java。它可以让您更好地了解引擎盖下发生的事情。这是两个反编译的函数(再次使用IntelliJ):
private static final boolean isPrimeNumber1(int number) {
if (number <= 1) {
return false;
} else {
int divider = 2;
int var2 = number / 2;
if (divider <= var2) {
while (true) {
if (number % divider == 0) {
return false;
}
if (divider == var2) {
break;
}
++divider;
}
}
return true;
}
}
private static final boolean isPrimeNumber2(int number) {
if (number <= 1) {
return false;
} else {
byte var1 = 2;
Sequence $this$any$iv =
SequencesKt.map(
CollectionsKt.asSequence((Iterable) (new IntRange(var1,number / 2))),(Function1) null.INSTANCE);
int $i$f$any = false;
Iterator var3 = $this$any$iv.iterator();
boolean var10000;
while (true) {
if (var3.hasNext()) {
Object element$iv = var3.next();
int it = ((Number) element$iv).intValue();
int var6 = false;
if (number % it != 0) {
continue;
}
var10000 = true;
break;
}
var10000 = false;
break;
}
return !var10000;
}
}
最后的注释:正如其他人提到的那样,要获得有意义的性能评估,您需要使用类似jmh
的工具。但是,根据经验,较简单的语言构造(例如,序列上的常规for / while循环)由于其提供的抽象级别较低,往往具有较少的开销。
Uktu的答案涵盖了为什么(是的,sequence
代码在这里不是最佳选择),但是通常来说-Sequence
是对Iterable
的补充。它们使用相同的功能,您可以将它们链接到处理管道中。
区别在于序列懒惰地执行,每个项目在处理下一个项目之前都要通过完整的管道,并且仅在需要处理时才对其进行处理。
例如,选中this extremely good and plausible code:
import kotlin.system.measureTimeMillis
fun isMagicNumber(num: Double) = num == 10000.0
fun main(args: Array<String>) {
val numberStrings = (1..25000).map(Int::toString)
val itertime = measureTimeMillis {
numberStrings
.map(String::toInt).map { it * 2.0 }.first(::isMagicNumber)
}
println("Iterable version: $itertime ms")
val seqtime = measureTimeMillis {
numberStrings.asSequence()
.map(String::toInt).map { it * 2.0 }.first(::isMagicNumber)
}
print("Sequence version: $seqtime ms")
}
从 25,000 个数字列表开始(如字符串),管道为
- 映射到
Int
- 将其加倍(转换为
Double
) - 获取第一个符合条件的(在这种情况下,等于10,000)
在进行下一步之前,Iterable
版本对整个列表执行每个步骤:
List<String> -> List<Int> -> List<Double> -> find element
它每次都会创建一个新列表(占用内存),并且直到创建了三个列表并且可以开始遍历最后一个列表时才开始检查元素-这是最早可以返回结果的列表。
序列执行此操作
String -> Int -> Double -> check
每个项目。一旦它碰到了符合检查条件的项目,就完成了。那一定要快得多吧!
Iterable version: 41 ms
Sequence version: 111 ms
啊!好。尽管如此,
转出序列会产生开销(这就是为什么如果您可以编写一个真正的基本for
循环会消除它们的原因),而且计算机也非常擅长对事物进行迭代-在此之下有很多优化在引擎盖上,创建新数组并对其进行迭代比使用链表等要快。这就是为什么如果要提高效率的话,您确实需要分析正在执行的操作。
如果源列表(以及Iterable
版本创建的所有其他列表)增大了 10倍, 250,000 个项目怎么办?
Iterable version: 260 ms
Sequence version: 155 ms
哦,你好,现在我们到了某个地方。事实证明,所有这些开销会在一段时间后开始堆积,并且能够尽早退出变得很重要,并且序列开始变得更加高效。
这只是测量时间-您还需要查看内存使用情况。构建庞大的列表可能会很快占用大量内存,甚至变得无法运行(如果您正在执行Euler项目级别的“使此工作用于大量的不可思议的物品”)。序列及其一次一物的方法可以使整个事情可行,并且在宇宙热死之前就可以完成
序列也可以是无限的!哪个限制了您可以使用它们的位置,或者限制了某些操作的效率(last()
需要在整个序列中运行),但是它也可以用于固定大小的集合不起作用的情况。>
是的,是的,使用正确的工具完成工作,并确保如果效率很重要,那么您实际上是在使用可提供最佳结果的版本。但是有时可读性和可组合性更重要,并且能够将操作链接起来做某事比平均和精简的for
循环好
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。