3

I needed to separate integers into 3-digit numbers and looked into the formatted() method of the Decimal type.
Calling the formatted() method of type Decimal 100,000 times each for a random integer gradually degrades performance.
I would like to know why this happens.

import Foundation

/// https://stackoverflow.com/a/56381954
func calculateTime(block : (() -> Void)) {
  let start = DispatchTime.now()
  block()
  let end = DispatchTime.now()
  let nanoTime = end.uptimeNanoseconds - start.uptimeNanoseconds
  let timeInterval = Double(nanoTime) / 1_000_000_000
  print("Time: \(timeInterval) seconds")
}

calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }
calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }
calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }
calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }
calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }
calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }
calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }
calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }
Time: 0.9492465 seconds
Time: 3.29213125 seconds
Time: 7.988363667 seconds
Time: 15.165178292 seconds
Time: 17.305036583 seconds
Time: 25.0114935 seconds
Time: 35.746310417 seconds
Time: 47.024551125 seconds
Mac
  • 137
  • 1
  • 8
  • 2
    Related: https://stackoverflow.com/q/25860942/1187415, https://stackoverflow.com/q/50668015/1187415 – Martin R Jan 12 '23 at 13:38

3 Answers3

6

While matt and Martin R are correct, they didn't actually point to the culprit. When you use the "Memory Object" Debugger, while the functions are executing, you will see this very interesting graph:

enter image description here

So, the function formatted() causes a LOT of allocations, mainly objects of type NSAutoLocale.

This hints to further issues: since evaluating a locale requires to access a file where the locale is actually read from, you may also experience very slow performance.

So, if you want to speed it up, too, you should explicitly use a pre-configured locale object, which you allocate once, and only once, which you then use as a parameter to format the string.

Edit:

In order to prove my hypothesis, I created an equivalent statement and compared that with your original:

Your original code:

calculateTime { for _ in 0...100_000 { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } }

Time: 9.674371748 seconds

The equivalent code using a single locale:

let locale = Locale()
calculateTime { for _ in 0...100_000 { _ = NSDecimalNumber(
    value: Int.random(in: 0...Int.max))
    .description(withLocale: locale)
} }

Time: 0.210493037 seconds

AND without any further modifications, calling the improved statement repeatedly, you get this:

Time: 0.224133942 seconds
Time: 0.238930039 seconds
Time: 0.214735965 seconds
Time: 0.220390686 seconds
Time: 0.212360066 seconds
Time: 0.207630215 seconds
Time: 0.205125154 seconds

...

ccjensen
  • 4,578
  • 2
  • 23
  • 25
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • I did in fact hypothesize something like this in a now deleted comment. :) But I didn't explore further as you have. Nice. – matt Jan 12 '23 at 15:50
  • Thank you for such in-depth research. It seems that in my environment the `description` method returns the number as is and not as a 3-digit comma delimited number. – Mac Jan 12 '23 at 15:51
  • 1
    @Mac maybe there is way to use a custom Formatter (with a your custom locale) for Decimals. But keep in mind, that for all formatting tasks, a locale is required. When you do "mass" formatting, ensure you can use a single Locale, or alternatively, use a single configured Formatter object, somehow in the format function. – CouchDeveloper Jan 12 '23 at 15:57
  • You definitely want to make just one formatter object if you're going to use one. They are cheap to use but expensive to make. – matt Jan 12 '23 at 17:50
5

It's a Heisenbug: you are causing the slowdown yourself by straining memory with your test harness. Change your test lines to:

calculateTime { for _ in 0...100_000 { autoreleasepool { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } } }
calculateTime { for _ in 0...100_000 { autoreleasepool { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } } }
calculateTime { for _ in 0...100_000 { autoreleasepool { _ = Decimal(Int.random(in: 0...Int.max)).formatted() } } }
// ... and so on
matt
  • 515,959
  • 87
  • 875
  • 1,141
1

Your formatter uses auto-released strings. By the time of your last call there are already 700,000 auto released strings in an auto release pool. That will slow you down.

Plus the formatting will produce lots of auto-released formattting objects, making it worse.

gnasher729
  • 51,477
  • 5
  • 75
  • 98