【问题标题】:Why is this extension function slower than non extension counterpart?为什么这个扩展功能比非扩展功能慢?
【发布时间】:2022-01-09 00:38:17
【问题描述】:

我正在尝试编写一个并行映射扩展函数来使用协程对列表进行并行映射操作。 但是,我的解决方案有很大的开销,我不知道为什么。

这是我对pmap扩展功能的实现:

fun <T, U> List<T>.pmap(scope: CoroutineScope = GlobalScope,
                    transform: suspend (T) -> U): List<U> {
    return map { i -> scope.async { transform(i) } }.map { runBlocking { it.await() } }
}

但是,当我在普通函数中执行完全相同的操作时,最多需要额外的 100 毫秒(这很多)。 我尝试使用内联,但没有效果。

我将在这里留下我为演示此行为所做的完整测试:

import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

fun main() {
    test()
}

fun <T, U> List<T>.pmap(scope: CoroutineScope = GlobalScope,
                    transform: suspend (T) -> U): List<U> {
    return this.map { i -> scope.async { transform(i) } }.map { runBlocking { it.await() } }
}

fun test() {
    val list = listOf<Long>(100,200,300)

    val transform: suspend (Long) -> Long = { long: Long ->
        delay(long)
        long*2
    }

    val timeTakenPmap = measureTimeMillis {
        list.pmap(GlobalScope) { transform(it) }
    }

    val manualpmap = measureTimeMillis {
        list.map { GlobalScope.async { transform(it) } }
            .map { runBlocking { it.await() } }
    }

    val timeTakenMap = measureTimeMillis {
        list.map { runBlocking { transform(it) } }
    }

    println("pmapTime: $timeTakenPmap - mapTime: $timeTakenMap - manualpmap: $manualpmap")
}

可以在 kotlin 操场上运行:https://pl.kotl.in/CIXVqezg3

在操场上,它会打印以下结果: pmapTime: 411 - mapTime: 602 - manualpmap: 302

MapTime 和 manualPmap 给出了合理的结果,延迟之外只有 2ms 的时间。但是 pmapTime 很遥远。而且manualpmap和pmap之间的代码在我看来完全一样。

在我自己的机器上它运行得更快一些,pmap 大约需要 350 毫秒。

有人知道为什么会这样吗?

【问题讨论】:

  • 这不是对代码进行基准测试的有效方法。一方面,您忽略了预热时间。第一次开始创建协程时,必须创建线程。后续协程可以重用池中的线程实例。你的工作量只有三个项目是微不足道的。查看基准测试库。
  • 哦,谢谢。我完全忘记了线程创建时间。我完全确定这就是原因。谢谢。

标签: kotlin kotlin-coroutines kotlin-extension


【解决方案1】:

首先,像这样的手动基准测试通常意义不大。编译器或 JIT 可以优化掉很多东西,任何结论都可能是完全错误的。如果你真的想比较事物,你应该改用考虑到 JVM 预热等的基准测试库。

现在,您看到的开销(如果您可以确认存在实际开销)可能是由于您的高阶扩展未标记为inline,因此需要创建您传递的 lambda 实例- 但正如@Tenfour04 所指出的,还有许多其他可能的原因:线程池延迟初始化、列表大小的重要性等。

话虽如此,这确实不是编写并行映射的合适方式,原因如下:

  • GlobalScope 通常是一个非常糟糕的默认值,应该只在非常特定的情况下使用。但是不要因为下一点而担心。
  • 如果您启动的协程没有超过您的方法,则不需要外部提供的CoroutineScope。相反,请使用coroutineScope { ... } 并让您的函数暂停,调用者将在需要时选择上下文
  • map { it.await() } 在出现错误时效率低下:如果最后一个元素的转换立即失败,map 将等待所有先前的元素完成后再失败。您应该更喜欢 awaitAll 来解决这个问题。
  • runBlocking 应避免在协程中使用(通常阻塞线程,尤其是当您不控制要阻塞的线程时),因此在像这样的深层库类函数中使用它是危险的,因为它可能会用于协程在某个时候。

应用这些点给出:

suspend inline fun <T, U> List<T>.pmap(transform: suspend (T) -> U): List<U> {
    return coroutineScope {
        map { async { transform(it) } }.awaitAll()
    }
}

【讨论】:

  • 另外,被测试的列表中只有 3 个项目。
  • 原因完全是线程创建时间。
猜你喜欢
  • 2021-11-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-10
  • 2020-02-14
  • 2018-02-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多