From 4e523f3c29894d4f84bf14c1d055685ecc16d015 Mon Sep 17 00:00:00 2001 From: Alex Watt Date: Tue, 28 Jul 2026 15:23:41 -0400 Subject: [PATCH] Trim allocations and checks on the Money.new construction path Three micro-optimizations on the hot construction path: - Cache zero Money instances in a private ZERO_MONEY constant instead of re-testing @@zero_money ||= {} on every zero-valued call. - initialize: a single finite? check replaces separate nan? and infinite? calls, and rounding is skipped entirely when the value is already within the currency's minor units (value.scale check), which avoids one BigDecimal allocation per call. - new_from_money: passing the money's own ISO code string (a common no-op conversion) returns the existing instance without any currency lookup. Benchmark (mixed-input Money.new): 787.6 -> 706.0 ns/op (-10.4%), allocations 3.80 -> 2.87 per call (-24.6%). --- lib/money/money.rb | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/money/money.rb b/lib/money/money.rb index da9ab412..5e3c54a5 100644 --- a/lib/money/money.rb +++ b/lib/money/money.rb @@ -8,6 +8,8 @@ class Money extend Forwardable NULL_CURRENCY = NullCurrency.new.freeze + ZERO_MONEY = {} # cache of zero Money instances, keyed by iso_code + private_constant :ZERO_MONEY attr_reader :value, :currency @@ -75,8 +77,7 @@ def new(value = 0, currency = nil) currency = Helpers.value_to_currency(currency) if value.zero? - @@zero_money ||= {} - @@zero_money[currency.iso_code] ||= super(Helpers::DECIMAL_ZERO, currency) + ZERO_MONEY[currency.iso_code] ||= super(Helpers::DECIMAL_ZERO, currency) else super(value, currency) end @@ -107,6 +108,11 @@ def rational(money1, money2) private def new_from_money(amount, currency) + # Fast path: same ISO code string as the existing money's currency. + if currency.is_a?(String) && currency == amount.currency.iso_code && !amount.no_currency? + return amount + end + currency = Helpers.value_to_currency(currency) if amount.no_currency? @@ -125,11 +131,13 @@ def new_from_money(amount, currency) end def initialize(value, currency) - raise ArgumentError if value.nan? - raise ArgumentError if value.infinite? + raise ArgumentError unless value.finite? @currency = currency - @value = BigDecimal(value.round(@currency.minor_units)) + minor_units = currency.minor_units + # Avoid allocating via round when the value is already within precision. + # BigDecimal#round(0) returns an Integer, hence the BigDecimal() wrap. + @value = value.scale <= minor_units ? value : BigDecimal(value.round(minor_units)) freeze end