Почему использование lazy замедляет мой код Kotlin

#kotlin

#котлин

Вопрос:

У меня есть некоторый код, который генерирует большое количество экземпляров небольшого класса данных. Когда я добавил одно ленивое свойство в класс, я заметил, что создание экземпляров этого класса стало намного медленнее, даже если к ленивому свойству никогда не обращаются. Как это происходит? Я ожидал, что не будет никакой разницы, если к ленивому свойству никогда не будет доступа. Есть ли какой-нибудь способ использовать отложенные свойства без такого снижения производительности?

Вот минимальный пример:

 import kotlin.system.measureTimeMillis

class LazyTest(val x: Int) {
    val test: Int by lazy { 9 }
}

fun main(){
    val time = measureTimeMillis { List(500_000) {LazyTest(it) }}
    println("time: $time")
}
 

При запуске этого на on play.kotlinlang.org это занимает 500-600 миллисекунд, если я закомментирую строку val test: Int by lazy { 9 } , ее выполнение займет около 40 миллисекунд.

Комментарии:

1. Использование by lazy создает второй объект для удержания лени, поэтому, конечно, потребуется больше работы. (Но кроме того, это проблемный тест, он не будет точно отражать производительность для реальных приложений и play.kotlinlang.org почти наверняка является ужасным способом измерения производительности.)

2. Я согласен, что play.kotlinlang.org это не очень хорошо для бенчмаркинга, но поскольку я видел то же самое в своем примере использования в реальном мире (решение 11-го дня 2020 года головоломки Advent of Code), я подумал, что лучше опубликовать что-то минимальное, что все еще можно наблюдать, а не мой полный код. Имеет смысл, что объект для удержания лени требует дополнительной работы, и это, по-видимому, является причиной здесь. Если я заменю свойство lazy другим объектом и увижу аналогичное замедление. Не стесняйтесь оставлять свой комментарий в качестве ответа, чтобы я мог принять его.

Ответ №1:

Использование by lazy создает второй объект для «удержания лени», реализуя отложенное вычисление. Это, скорее всего, ответственно за замедление.

Ответ №2:

lazy делегаты по умолчанию используют синхронизацию для обеспечения потокобезопасности:

По умолчанию оценка отложенных свойств синхронизирована: значение вычисляется только в одном потоке, и все потоки будут видеть одно и то же значение. Если синхронизация делегата инициализации не требуется, чтобы несколько потоков могли выполнять его одновременно, передайте LazyThreadSafetyMode.PUBLICATION в качестве параметра функции lazy() . И если вы уверены, что инициализация всегда будет происходить в том же потоке, что и тот, в котором вы используете свойство, вы можете использовать LazyThreadSafetyMode.NONE : это не влечет за собой никаких гарантий потокобезопасности и связанных с этим накладных расходов.

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

На самом деле это не относится к вашему примеру, поскольку вы фактически не получаете доступ к своим свойствам (вы просто создаете полмиллиона объектов, содержащих полмиллиона делегатов, что занимает некоторое время!) но это хорошая вещь, о которой нужно знать, когда вы их используете.

Ответ №3:

Когда вы ЗНАЕТЕ, что доступ будет потокобезопасным, вы можете использовать это для повышения производительности:

 fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer)