#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)