Как max () иногда может быть намного быстрее на последовательности одинаковой длины? - PullRequest
0 голосов
/ 08 мая 2019

Я задаю этот вопрос исключительно, чтобы лучше понять, как работают последовательности kotlin. Я думал, что у меня есть твердое понимание, но я не могу объяснить то, что я наблюдал в коротком тесте, тем, что я знаю, поэтому очевидно, что у меня где-то есть неправильное представление.

Моя цель состояла в том, чтобы сделать быстрый тест для сравнения производительности списков и последовательностей при фильтрации по критерию, а затем получить максимальное значение результата. Это операция, которая встречается довольно часто в некотором коде, который у меня есть, и я пытаюсь решить, стоит ли переписывать его, чтобы использовать последовательности вместо списков. Кажется, что так будет, потому что последовательность всегда быстрее, но здесь вопрос не в этом.

Скорее, я бы попросил вас объяснить мне, как может возникнуть описанный ниже «артефакт».

Прежде всего, вот полный тест, который я провел:

fun `just checking the performance of sequences`() {
    val log = logger()
    var totaldif = 0L
    var seqFasterThanList = 0
    (1..1000).forEach {
        val testList = (1..6000000).toList().shuffled()
        val testSequence = testList.asSequence()

        // filter and find max
        val listDuration = measureTimeMillis {
            testList.filter { it % 2 == 0 }.max()
        }
        val sequenceDuration = measureTimeMillis {
            testSequence.filter { it % 2 == 0 }.max()
        }

        log.info("List: {} ms; Sequence: {} ms;", listDuration, sequenceDuration)
        if (sequenceDuration < listDuration) {
            seqFasterThanList++
            totaldif += (listDuration - sequenceDuration)
        }
    }

    log.info("sequence was faster {} out of 1000 runs. average difference: {} ms",
            seqFasterThanList, totaldif / seqFasterThanList)
}

Результаты в основном выглядели так:

List: 299 ms; Sequence: 217 ms;
List: 289 ms; Sequence: 211 ms;
List: 288 ms; Sequence: 220 ms;

Кроме того, время от времени, примерно 1 к 20, результат выглядел примерно так:

List: 380 ms; Sequence: 63 ms

Как видите, в этих случаях операция была значительно быстрее. Такого рода поведение я ожидал бы при выполнении таких операций, как find или first, которые могут закончиться рано, когда они найдут совпадение. Но по самой своей природе max имеет , чтобы пройти всю последовательность, чтобы гарантировать результат. Так как же возможно, что иногда он может найти результат более чем в 3 раза быстрее, чем обычно требуется, с одинаковым количеством элементов для прохождения?

1 Ответ

4 голосов
/ 08 мая 2019

Далее мой оригинальный ответ, который, как указал @Slaw, на самом деле не отвечал на ваш вопрос (он объяснял, почему Sequence.filter быстрее, чем Iterable.filter, а не почему Sequence.filter, кажется, периодически быстрее чем это обычно). Тем не менее, я оставляю это ниже, поскольку это связано с тем, что я думаю, может быть ответом на ваш актуальный вопрос.

Полагаю, это может быть связано со сборкой мусора. Как вы можете видеть из моего первоначального ответа, когда вы вызываете Iterable.filter, вы вызываете копирование множества массивов, то есть вы помещаете в память много вещей, которые необходимо очистить в определенные моменты. Интересно, что это за очистка памяти, созданная тестами List, которая действительно вызывает аномалии? Я думаю, что может случиться так, что сборщик мусора все время включается и выполняет полную сборку: это приводит к замедлению теста List до более медленного, чем обычно. И после этого запуска память очищается, возможно, поэтому тест Sequence быстрее в этот раз.

И я подозреваю, что это связано со сборкой мусора, потому что я воспроизвел ваши аномалии, а затем внес одно изменение: вместо вызова testList.filter я вызываю testList.filterTo, передавая ArrayList того же размера, что и список , Это означает, что копирование массива не требуется, а также создание ArrayList теперь не по времени:

val arrayList = ArrayList<Int>(testList.size)
val listDuration = measureTimeMillis {
    testList.filterTo(arrayList) { it % 2 == 0 }.max()
}

Как только я это сделал, аномалии исчезли. Может быть, вы можете проверить в своей системе и посмотреть, не заставит ли это аномалии исчезнуть там же. Это прерывисто, поэтому немного трудно узнать наверняка.

Конечно, это не доказывает, что это сборка мусора, но я думаю, что это делает его возможным виновником. Вы можете включить ведение журнала GC, чтобы узнать, хотите ли вы знать наверняка. Если вы это сделаете, дайте нам знать, что вы найдете: было бы интересно услышать ваши результаты.


Оригинальный ответ ниже (объясняющий, почему Iterable.filter медленнее Sequence.filter)

Если вы посмотрите на исходный код Iterable<T>.filter, вы увидите, что он делает это:

public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}

Создает новый ArrayList, затем зацикливает элементы, проверяя предикат для каждого и добавляя их в этот список массивов, если они соответствуют предикату. Это означает, что для каждого элемента X (независимо от размера по умолчанию списка массива) список массивов должен изменить свой размер, чтобы в него можно было добавить больше элементов (т.е. создать новую копию базового массива, в котором хранятся все его данные).

В последовательности код другой:

public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
    return FilteringSequence(this, true, predicate)
}

Здесь нет некоторого базового массива, хранящего все элементы, поэтому копирование массивов не требуется. Вместо этого есть просто Iterator, который будет возвращать следующий элемент, который соответствует предикату всякий раз, когда вызывается next.

Подробно о том, как это реализовано, в классе FilteringSequence:

internal class FilteringSequence<T>(
    private val sequence: Sequence<T>,
    private val sendWhen: Boolean = true,
    private val predicate: (T) -> Boolean
) : Sequence<T> {

    override fun iterator(): Iterator<T> = object : Iterator<T> {
        val iterator = sequence.iterator()
        var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
        var nextItem: T? = null

        private fun calcNext() {
            while (iterator.hasNext()) {
                val item = iterator.next()
                if (predicate(item) == sendWhen) {
                    nextItem = item
                    nextState = 1
                    return
                }
            }
            nextState = 0
        }

        override fun next(): T {
            if (nextState == -1)
                calcNext()
            if (nextState == 0)
                throw NoSuchElementException()
            val result = nextItem
            nextItem = null
            nextState = -1
            @Suppress("UNCHECKED_CAST")
            return result as T
        }
...