Вам не хватает стековой памяти.
Рассмотрим эту очень простую рекурсивную функцию для сложения целых чисел от 1 до n
:
func sum(to n: Int) -> Int {
guard n > 0 else { return 0 }
return n + sum(to: n - 1)
}
Вы обнаружите, что если попытаетесь, например, суммировать числа от 1 до100 000, приложение будет аварийно завершать работу как в выпускных, так и в отладочных сборках, но просто будет аварийно завершать работу при отладочных сборках.Я подозреваю, что в отладочных сборках в стек помещается только больше диагностической информации, из-за чего в стеке становится меньше места.В сборках релиза, описанных выше, указатель стека продвигается на 0x20 байт каждый рекурсивный вызов, тогда как отладочная сборка продвигается на 0x80 байт каждый раз.И если вы делаете что-то существенное в своей рекурсивной функции, эти приращения могут быть больше, и сбой может произойти с еще меньшим количеством рекурсивных вызовов.Но размер стека на моем устройстве (iPhone Xs Max) и на моем симуляторе (Thread.current.stackSize
) составляет 524 288 байт, и это соответствует величине, на которую продвигается указатель стека, и максимальному числу рекурсивныхзвонки, которые я могу сделать.Если ваше устройство выходит из строя раньше, чем симулятор, возможно, ваше устройство имеет меньше оперативной памяти и поэтому выделило меньшую stackSize
.
Итог, вы можете реорганизовать свой алгоритм в нерекурсивный, если выхотите наслаждаться высокой производительностью, но не хотите использовать огромные стеки вызовов.Кроме того, нерекурсивное представление выше было на порядок быстрее, чем рекурсивное представление.
В качестве альтернативы, вы можете отправлять свои рекурсивные вызовы асинхронно, что устраняет проблемы с размером стека, но приводит к накладным расходам GCD,Асинхронное воспроизведение описанного выше было на два-три порядка медленнее, чем простое рекурсивное представление, и, очевидно, еще на один порядок медленнее, чем итеративное представление.
По общему признанию, мой простой метод sum
настолько тривиален, что накладные расходы на рекурсивные вызовы начинают составлять значительную часть общего времени вычислений, и, учитывая, что ваша процедура может показаться более сложной, я подозреваю,разница будет менее резкой.Тем не менее, если вы хотите избежать исчерпания стекового пространства, я бы просто предложил использовать нерекурсивное представление.
Я бы отослал вас к следующим видеороликам WWDC:
Стоит отметить, что глубоко рекурсивные процедуры не всегда должныпотреблять большой стек.Примечательно, что иногда мы можем использовать tail-recursion , где наш рекурсивный вызов - самый последний сделанный вызов.Например, мой фрагмент выше не использует хвостовой вызов, потому что он добавляет n
к значению, возвращаемому рекурсивным вызовом.Но мы можем реорганизовать его для прохождения промежуточного итога, таким образом гарантируя, что рекурсивный вызов является истинным «хвостовым вызовом»:
func sum(to n: Int, previousTotal: Int = 0) -> Int {
guard n > 0 else { return previousTotal }
return sum(to: n - 1, previousTotal: previousTotal + n)
}
Сборки релиза достаточно умны, чтобы оптимизировать эту хвостовую рекурсию (через процесс, называемый«Оптимизация хвостового вызова», TCO, также известная как «устранение хвостового вызова»), уменьшая рост стека для рекурсивных вызовов.WWDC 2015 Профилирование по глубине , в то время как на другой теме, профилировщик времени, точно показывает, что происходит, когда он оптимизирует хвостовые вызовы.
Чистый эффект состоит в том, что если ваша рекурсивная процедура использует хвоствызовы, релизные сборки могут использовать удаление хвостовых вызовов для уменьшения проблем стековой памяти, но отладочные (неоптимизированные) сборки этого не сделают.