我正在尝试理解函数式编程,虽然代码看起来很漂亮,但我担心与命令式实现相比会有性能损失。
然而,我完全惊讶地发现功能实现比我的命令式实现(看起来很难看)要快得多。
现在,我确信在我的命令式实施中存在一些错误,但是,我不确定这个错误是什么..
一些基准: 功能大小为35个元素: 152954779 ns
迫害35: 198337749325 ns
即使我在列表中添加10个元素,这也会恶化
代码在kotlin:
势在必行:
fun quickSort(numbers: IntArray, l: Int, r: Int): IntArray {
if (l >= r)
return numbers
fun swap(m: Int, n: Int) {
val temp = numbers[m]
numbers[m] = numbers[n]
numbers[n] = temp
}
var i = l + 1
var j = l + 1
val pivot = numbers[l]
while (j < r) {
if (numbers[j] < pivot) {
if (numbers[i] > pivot) {
swap(i, j)
}
i++
}
j++
}
swap(l, i - 1)
quickSort(numbers, 0, i - 1)
quickSort(numbers, i, r)
return numbers
}
我相信我可以重构并改进它,但是,这不是我现在的目标。
势在必行2:
fun partitionTest(arr: IntArray, left: Int, right: Int): Int {
var i = left
var j = right
var tmp: Int
val pivot = arr[(left + right) / 2]
while (i <= j) {
while (arr[i] < pivot)
i++
while (arr[j] > pivot)
j--
if (i <= j) {
tmp = arr[i]
arr[i] = arr[j]
arr[j] = tmp
i++
j--
}
}
return i
}
fun quickSortTest(arr: IntArray, left: Int, right: Int) {
val index = partitionTest(arr, left, right)
if (left < index - 1)
quickSort(arr, left, index - 1)
if (index < right)
quickSort(arr, index, right)
}
功能:
fun functionalQuickSort(numbers: IntArray): IntArray {
return when {
numbers.size <= 1 -> numbers
else -> {
val pivotIndex = 0
functionalQuickSort(numbers.filter { it < numbers[pivotIndex] }.toIntArray()) + numbers[pivotIndex] + functionalQuickSort(
numbers.filter { it > numbers[pivotIndex] }.toIntArray()
)
}
}
}
主:
val numbers = Random().ints(10).toArray()
var start = System.nanoTime()
functionalQuickSort(numbers).also { println(it.contentToString()) }
var end = System.nanoTime()
println("Took ${end - start}")
start = System.nanoTime()
quickSort(numbers,0,numbers.size).also { println(it.contentToString()) }
end = System.nanoTime()
println("Took ${end - start}")
答案 0 :(得分:4)
我使用了一个已知良好的命令式QuickSort算法而不是你的算法,这看起来很糟糕。我的分区代码在结构上与您的不同,因为它使用C.A.R. Hoare的原始方案,而你的似乎是使用Lomuto方案(因其简单而非流行而非流行)。
我还编写了代码来处理大多数JVM微基准测试问题。这是:
import java.util.concurrent.ThreadLocalRandom
import kotlin.system.measureTimeMillis
const val PROBLEM_SIZE = 1_000_000L
fun quickSort(array: IntArray, lo: Int, hi: Int) {
if (lo >= hi) {
return
}
val p = partition(array, lo, hi)
quickSort(array, lo, p)
quickSort(array, p + 1, hi)
}
private fun partition(array: IntArray, lo: Int, hi: Int): Int {
val pivot = array[(lo + hi) / 2]
var i = lo - 1
var j = hi + 1
while (true) {
do {
i++
} while (array[i] < pivot)
do {
j--
} while (array[j] > pivot)
if (i >= j) {
return j
}
array[i] = array[j].also { array[j] = array[i] }
}
}
fun functionalQuickSort(numbers: IntArray): IntArray {
return when {
numbers.size <= 1 -> numbers
else -> {
val pivotIndex = 0
functionalQuickSort(numbers.filter { it < numbers[pivotIndex] }.toIntArray()) +
numbers[pivotIndex] +
functionalQuickSort(numbers.filter { it > numbers[pivotIndex] }.toIntArray()
)
}
}
}
fun main(args: Array<String>) {
benchmark("imperative", ::runImperativeQuickSort)
benchmark("functional", ::functionalQuickSort)
}
fun benchmark(name: String, block : (IntArray) -> IntArray) {
println("Warming up $name")
(1..4).forEach {
validate(block(randomInts()))
}
println("Measuring")
val average = (1..10).map {
var result: IntArray? = null
val input = randomInts()
val took = measureTimeMillis {
result = block(input)
}
validate(result!!)
took
}.average()
println("An average $name run took $average ms")
}
private fun runImperativeQuickSort(array: IntArray): IntArray {
quickSort(array, 0, array.size - 1)
return array
}
private fun randomInts() = ThreadLocalRandom.current().ints(PROBLEM_SIZE).toArray()
private fun validate(array: IntArray) {
var prev = array[0]
(1 until array.size).forEach {
array[it].also { curr ->
require(curr >= prev)
prev = curr
}
}
}
典型输出:
Warming up imperative
Measuring
An average imperative run took 106.6 ms
Warming up functional
Measuring
An average functional run took 537.4 ms
所以......不,功能版本不会更快。
答案 1 :(得分:2)
我花了一段时间才找到它,但你的递归调用中有一个错误:
quickSort(numbers, 0, i - 1)
这应该是:
quickSort(numbers, l, i - 1)
^
作为一个小优化,您也可以提前返回长度为1的段(除了长度为0):
if (l + 1 >= r)
return numbers
似乎还有一些我没有详细研究过的问题。 if
循环中的嵌套while
看起来很狡猾;我认为可以删除内部if
:
while (j < r) {
if (numbers[j] < pivot) {
swap(i, j)
i++
}
j++
}
仔细考虑你的不变量是什么以及每个语句是否维护它们。
通过这些调整,命令式版本在100000个元素上的运行速度提高了约10倍。
还要考虑如果两个元素相等会发生什么,这对于如此小的数组是不太可能的,但对于100000个元素的数组(生日悖论)会发生。在这种情况下,您会发现您的功能实现已被破坏。
关于基准测试的主题: