-
Notifications
You must be signed in to change notification settings - Fork 446
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Quick money (butchering this library for the glory of satan, and CPU cycles) #634
Conversation
In this change, we bumped relevant dependencies to stick to `php:^7.3` as minimum supported `php` version (since `7.2` is EOL'd and no longer covered by security fixes). To do that, we upgraded all dependencies, upgraded spec files to comply with newest phpspec. This currently means that we have a few broken scenarios and failures: ``` ---- failed examples Money/Parser/DecimalMoneyParser 47 ✘ throws an exception when money includes currency symbol expected exception of class "Money\Exception\ParserExc...", but got [exc:PhpSpec\Exception\Example\ErrorException("16384: Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a Money\Currency instance instead. in /home/ocramius/Documents/moneyphp/money/src/Parser/DecimalMoneyParser.php line 50")]. Money/Parser/DecimalMoneyParser 52 ✘ throws an exception when money is not a valid decimal expected exception of class "Money\Exception\ParserExc...", but got [exc:PhpSpec\Exception\Example\ErrorException("16384: Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a Money\Currency instance instead. in /home/ocramius/Documents/moneyphp/money/src/Parser/DecimalMoneyParser.php line 50")]. ---- broken examples Money/Exchange/ExchangerExchange 27 ! is initializable could not reflect class Exchanger\Exchanger as it is marked final. Money/Exchange/ExchangerExchange 32 ! is an exchange could not reflect class Exchanger\Exchanger as it is marked final. Money/Exchange/ExchangerExchange 37 ! exchanges currencies could not reflect class Exchanger\Exchanger as it is marked final. Money/Exchange/ExchangerExchange 53 ! throws an exception when cannot exchange currencies could not reflect class Exchanger\Exchanger as it is marked final. Money/Parser/DecimalMoneyParser 31 ! parses money 16384: Passing a currency as string is deprecated since 3.1 and will be removed in 4.0. Please pass a Money\Currency instance instead. in /home/ocramius/Documents/moneyphp/money/src/Parser/DecimalMoneyParser.php line 50 29 specs 185 examples (178 passed, 2 failed, 5 broken) 146ms ```
…sed, then they should be raised No suppressing them, since otherwise they effectively go unnoticed. Having a suppressed `trigger_error()` means that only side-effects get run, instead of proper exception conversion.
… number operations These benchmarks cover the "most used" functionality of `Money`, and should highlight any computational complexity overhead.
…nd `Money::avg()`
@sagikazarmark In my opinion these changes should target MoneyPHP 4.0 with PHP8.0+. This would mean that @Ocramius has the freedom to remove deprecation warnings and add types where possible. So then we have Money 3 for <= PHP7.4. I would then also create a separate branch for the development of 4.0. Would you agree? |
@frederikbosch your maintenance role, your rules: remember that bumping dependencies is not a BC break, but this branch DOES contain BC breaks, so it is a good idea to release a new major anyway. Can I suggest to branch off |
@Ocramius That's why I asked the other maintainer @sagikazarmark to join the discussion, and see what he thinks. I would suggest to break dependencies. I know there are places in the library that still use floats for calculations; they should be removed in this cycle too. We could indeed branch off to a 3.x branch. Moreover I want to discuss how we should store numbers internally. There is this |
Working on this again today - will consider the changes as if I were on v4, so there will be BC breaks.
Yes, overall something I'd like to change: use |
Wonderful. My suggestion is to go for PHP8.0 and above, but I hope @sagikazarmark has the opportunity to confirm my suggestion. |
Works for me, and also makes my life easier with the whole type improvements. I don't think I'll wait for @sagikazarmark's input: that's mostly because I can (like everyone else) only dedicate limited time to this anyway, and I since v3 already supports PHP 7, there is no need to support it in v4. |
Alright, go for it then. You are the one willing to spend time on this huge improve, whereas @sagikazarmark and I are the ones that do not have this time, so I guess we are happy with the changes you provide! |
Sorry for being late to the party. Yeah, I think targeting 4.0 with these changes makes sense. I'm not sure about PHP 8.0, unless there is a specific reason (ie. a feature that we want to use, a dependency that only supports 8.0, etc). I can create a 3.x branch, so we can manage required checks separately. I wonder if submitting these changes in smaller chunks would be possible (eg. PHP upgrades and build changes then every other change). It would make reviews easier and would make sure that subsequent PRs run the new build pipelines. We should also discuss the fate of the current 4.0 milestone: https://github.com/moneyphp/money/milestone/4 Thanks for putting the effort into this @Ocramius! |
…and becoming unusable later on
…n internal functions Overall really hard to tell if the results are legit, but there are some improvements: ``` ❯ php -dopcache.enable_cli=1 ./vendor/bin/phpbench run --ref=original --retry-threshold=5 --iterations=10 PHPBench @git_tag@ running benchmarks... with configuration file: /home/ocramius/Documents/moneyphp/money/phpbench.json with PHP version 8.0.3, xdebug ❌, opcache ✔ \Benchmark\Money\NumberInstantiationBench benchConstructorWithZeroIntegerAmount...R1 I0 - [Mo575.630μs vs Mo591.581μs] -2.70% (±2.20%) benchConstructorWithPositiveIntegerAmou.R7 I9 - [Mo551.930μs vs Mo550.683μs] +0.23% (±3.02%) benchConstructorWithNegativeIntegerAmou.R1 I1 - [Mo554.108μs vs Mo580.812μs] -4.60% (±1.80%) benchConstructorWithZeroAndFractionalAm.R1 I3 - [Mo543.759μs vs Mo595.014μs] -8.61% (±1.77%) benchConstructorWithFractionalAmount....R1 I1 - [Mo589.360μs vs Mo611.898μs] -3.68% (±1.92%) benchConstructorWithNegativeFractionalA.R1 I0 - [Mo573.149μs vs Mo580.123μs] -1.20% (±2.22%) \Benchmark\Money\MoneyOperationBench benchAdd................................R1 I6 - [Mo999.123μs vs Mo987.689μs] +1.16% (±2.42%) benchSubtract...........................R1 I9 - [Mo985.683μs vs Mo936.192μs] +5.29% (±1.70%) benchMultiply...........................R1 I7 - [Mo1.003ms vs Mo1.057ms] -5.11% (±2.00%) benchDivide.............................R1 I9 - [Mo946.427μs vs Mo1.031ms] -8.19% (±3.11%) benchRatioOf............................R1 I2 - [Mo985.380μs vs Mo989.503μs] -0.42% (±2.17%) benchMod................................R2 I5 - [Mo391.671μs vs Mo401.023μs] -2.33% (±2.37%) benchIsSameCurrency.....................R1 I4 - [Mo2.000μs vs Mo2.000μs] 0.00% (±0.00%) benchIsZero.............................R10 I9 - [Mo384.644μs vs Mo426.642μs] -9.84% (±1.56%) benchAbsolute...........................R2 I9 - [Mo423.309μs vs Mo396.644μs] +6.72% (±2.35%) benchNegative...........................R6 I9 - [Mo981.397μs vs Mo983.110μs] -0.17% (±2.55%) benchIsPositive.........................R3 I9 - [Mo386.301μs vs Mo392.182μs] -1.50% (±2.37%) benchCompare............................R7 I8 - [Mo387.603μs vs Mo397.301μs] -2.44% (±3.22%) benchLessThan...........................R3 I9 - [Mo407.575μs vs Mo390.164μs] +4.46% (±2.31%) benchLessThanOrEqual....................R1 I9 - [Mo425.425μs vs Mo430.164μs] -1.10% (±1.34%) benchEquals.............................R1 I7 - [Mo2.000μs vs Mo3.000μs] -33.33% (±0.00%) benchGreaterThan........................R1 I7 - [Mo390.366μs vs Mo390.450μs] -0.02% (±2.10%) benchGreaterThanOrEqual.................R2 I9 - [Mo412.262μs vs Mo411.738μs] +0.13% (±1.99%) \Benchmark\Money\MoneyInstantiationBench benchConstructorWithZeroIntegerAmount...R2 I4 - [Mo904.501μs vs Mo898.483μs] +0.67% (±2.20%) benchConstructorWithPositiveIntegerAmou.R1 I8 - [Mo915.419μs vs Mo937.671μs] -2.37% (±2.11%) benchConstructorWithNegativeIntegerAmou.R1 I5 - [Mo896.722μs vs Mo909.286μs] -1.38% (±1.30%) benchConstructorWithZeroStringAmount....R2 I4 - [Mo908.315μs vs Mo933.039μs] -2.65% (±2.32%) benchConstructorWithPositiveStringAmoun.R1 I8 - [Mo890.975μs vs Mo857.589μs] +3.89% (±1.99%) benchConstructorWithNegativeStringAmoun.R1 I2 - [Mo858.750μs vs Mo904.536μs] -5.06% (±2.54%) Subjects: 29, Assertions: 0, Failures: 0, Errors: 0 ```
… removing useless test related to that
…Calculator()` all the time This is a minimal improvement on a very hot execution path
…ultiply()` and `#divide()` Also here, no noticeable improvements other than a massive jump in `benchRatioOf`: ``` ❯ php -dopcache.enable_cli=1 ./vendor/bin/phpbench run --ref=original --retry-threshold=5 --iterations=10 PHPBench @git_tag@ running benchmarks... with configuration file: /home/ocramius/Documents/moneyphp/money/phpbench.json with PHP version 8.0.3, xdebug ❌, opcache ✔ \Benchmark\Money\NumberInstantiationBench benchConstructorWithZeroIntegerAmount...R1 I0 - [Mo569.571μs vs Mo591.581μs] -3.72% (±1.94%) benchConstructorWithPositiveIntegerAmou.R2 I8 - [Mo599.841μs vs Mo550.683μs] +8.93% (±2.41%) benchConstructorWithNegativeIntegerAmou.R1 I4 - [Mo621.906μs vs Mo580.812μs] +7.08% (±2.40%) benchConstructorWithZeroAndFractionalAm.R1 I9 - [Mo570.344μs vs Mo595.014μs] -4.15% (±2.62%) benchConstructorWithFractionalAmount....R2 I3 - [Mo600.387μs vs Mo611.898μs] -1.88% (±2.04%) benchConstructorWithNegativeFractionalA.R1 I2 - [Mo590.276μs vs Mo580.123μs] +1.75% (±1.99%) \Benchmark\Money\MoneyOperationBench benchAdd................................R1 I3 - [Mo999.916μs vs Mo987.689μs] +1.24% (±1.97%) benchSubtract...........................R1 I9 - [Mo924.575μs vs Mo936.192μs] -1.24% (±2.18%) benchMultiply...........................R3 I8 - [Mo992.857μs vs Mo1.057ms] -6.09% (±2.46%) benchDivide.............................R1 I1 - [Mo1.015ms vs Mo1.031ms] -1.54% (±2.58%) benchRatioOf............................R5 I8 - [Mo381.761μs vs Mo989.503μs] -61.42% (±1.89%) benchMod................................R1 I9 - [Mo396.292μs vs Mo401.023μs] -1.18% (±2.16%) benchIsSameCurrency.....................R2 I8 - [Mo4.000μs vs Mo2.000μs] +100.00% (±0.00%) benchIsZero.............................R1 I3 - [Mo393.902μs vs Mo426.642μs] -7.67% (±1.72%) benchAbsolute...........................R1 I4 - [Mo379.667μs vs Mo396.644μs] -4.28% (±2.79%) benchNegative...........................R1 I3 - [Mo979.540μs vs Mo983.110μs] -0.36% (±2.10%) benchIsPositive.........................R1 I4 - [Mo408.442μs vs Mo392.182μs] +4.15% (±2.21%) benchCompare............................R1 I8 - [Mo381.159μs vs Mo397.301μs] -4.06% (±1.87%) benchLessThan...........................R1 I6 - [Mo391.462μs vs Mo390.164μs] +0.33% (±1.71%) benchLessThanOrEqual....................R4 I6 - [Mo416.380μs vs Mo430.164μs] -3.20% (±2.67%) benchEquals.............................R3 I4 - [Mo4.000μs vs Mo3.000μs] +33.33% (±0.00%) benchGreaterThan........................R1 I5 - [Mo382.789μs vs Mo390.450μs] -1.96% (±1.71%) benchGreaterThanOrEqual.................R1 I1 - [Mo379.530μs vs Mo411.738μs] -7.82% (±1.33%) \Benchmark\Money\MoneyInstantiationBench benchConstructorWithZeroIntegerAmount...R2 I9 - [Mo945.877μs vs Mo898.483μs] +5.27% (±2.50%) benchConstructorWithPositiveIntegerAmou.R1 I7 - [Mo887.787μs vs Mo937.671μs] -5.32% (±2.63%) benchConstructorWithNegativeIntegerAmou.R3 I7 - [Mo958.738μs vs Mo909.286μs] +5.44% (±2.60%) benchConstructorWithZeroStringAmount....R1 I0 - [Mo954.548μs vs Mo933.039μs] +2.31% (±2.40%) benchConstructorWithPositiveStringAmoun.R3 I9 - [Mo931.642μs vs Mo857.589μs] +8.64% (±2.42%) benchConstructorWithNegativeStringAmoun.R1 I2 - [Mo943.121μs vs Mo904.536μs] +4.27% (±1.88%) Subjects: 29, Assertions: 0, Failures: 0, Errors: 0 ```
Also added an `is_int()` special case to the constructor, since we often deal with integer values when constructing a `Money` instance, so we don't need to check for a more expensive `filter_var()` operation.
…nt equality is also non-matching This moves away a function call that is otherwise repeated very often, and which can lead to a lot of runtime overhead for no reason. Instead, we do this comparison only when the amount is the same (direct equality, faster for the engine).
While calling `Currency#equals(Currency)` may indeed look better from a domain perspective, `Currency` is `final`, and currency comparison is one of the most frequent operations performed on `Money` (for internal assertions). Therefore, removing the method call is both feasible and beneficial for performance: ``` ❯ php -dopcache.enable_cli=1 ./vendor/bin/phpbench run --ref=original --retry-threshold=5 --iterations=10 PHPBench @git_tag@ running benchmarks... with configuration file: /home/ocramius/Documents/moneyphp/money/phpbench.json with PHP version 8.0.3, xdebug ❌, opcache ✔ \Benchmark\Money\NumberInstantiationBench benchConstructorWithZeroIntegerAmount...R3 I7 - [Mo561.632μs vs Mo591.581μs] -5.06% (±3.01%) benchConstructorWithPositiveIntegerAmou.R1 I7 - [Mo542.669μs vs Mo550.683μs] -1.46% (±1.00%) benchConstructorWithNegativeIntegerAmou.R1 I2 - [Mo579.585μs vs Mo580.812μs] -0.21% (±1.60%) benchConstructorWithZeroAndFractionalAm.R3 I8 - [Mo574.055μs vs Mo595.014μs] -3.52% (±1.86%) benchConstructorWithFractionalAmount....R1 I9 - [Mo580.926μs vs Mo611.898μs] -5.06% (±2.09%) benchConstructorWithNegativeFractionalA.R2 I8 - [Mo587.192μs vs Mo580.123μs] +1.22% (±2.55%) \Benchmark\Money\MoneyOperationBench benchAdd................................R1 I8 - [Mo985.200μs vs Mo987.689μs] -0.25% (±2.61%) benchSubtract...........................R1 I8 - [Mo985.114μs vs Mo936.192μs] +5.23% (±2.70%) benchMultiply...........................R1 I0 - [Mo988.329μs vs Mo1.057ms] -6.52% (±2.69%) benchDivide.............................R1 I7 - [Mo933.611μs vs Mo1.031ms] -9.43% (±0.93%) benchRatioOf............................R4 I4 - [Mo392.845μs vs Mo989.503μs] -60.30% (±1.70%) benchMod................................R1 I3 - [Mo421.904μs vs Mo401.023μs] +5.21% (±1.72%) benchIsSameCurrency.....................R2 I6 - [Mo1.000μs vs Mo2.000μs] -50.00% (±0.00%) benchIsZero.............................R1 I7 - [Mo400.636μs vs Mo426.642μs] -6.10% (±1.00%) benchAbsolute...........................R10 I9 - [Mo397.953μs vs Mo396.644μs] +0.33% (±2.76%) benchNegative...........................R1 I6 - [Mo955.247μs vs Mo983.110μs] -2.83% (±2.45%) benchIsPositive.........................R1 I0 - [Mo405.106μs vs Mo392.182μs] +3.30% (±2.71%) benchCompare............................R3 I9 - [Mo401.284μs vs Mo397.301μs] +1.00% (±2.12%) benchLessThan...........................R1 I9 - [Mo390.481μs vs Mo390.164μs] +0.08% (±1.84%) benchLessThanOrEqual....................R1 I5 - [Mo395.329μs vs Mo430.164μs] -8.10% (±2.53%) benchEquals.............................R10 I9 - [Mo1.000μs vs Mo3.000μs] -66.67% (±0.00%) benchGreaterThan........................R1 I0 - [Mo414.980μs vs Mo390.450μs] +6.28% (±2.71%) benchGreaterThanOrEqual.................R1 I1 - [Mo385.041μs vs Mo411.738μs] -6.48% (±2.64%) \Benchmark\Money\MoneyInstantiationBench benchConstructorWithZeroIntegerAmount...R1 I9 - [Mo954.935μs vs Mo898.483μs] +6.28% (±1.18%) benchConstructorWithPositiveIntegerAmou.R1 I3 - [Mo913.027μs vs Mo937.671μs] -2.63% (±2.09%) benchConstructorWithNegativeIntegerAmou.R1 I0 - [Mo926.714μs vs Mo909.286μs] +1.92% (±2.25%) benchConstructorWithZeroStringAmount....R2 I9 - [Mo998.900μs vs Mo933.039μs] +7.06% (±2.79%) benchConstructorWithPositiveStringAmoun.R2 I4 - [Mo927.877μs vs Mo857.589μs] +8.20% (±2.17%) benchConstructorWithNegativeStringAmoun.R2 I9 - [Mo951.571μs vs Mo904.536μs] +5.20% (±2.64%) Subjects: 29, Assertions: 0, Failures: 0, Errors: 0 ```
…unt()` operation that can be optimized by opcache ``` ❯ php -dopcache.enable_cli=1 ./vendor/bin/phpbench run --ref=original --retry-threshold=5 --iterations=100 --filter=Avg PHPBench @git_tag@ running benchmarks... with configuration file: /home/ocramius/Documents/moneyphp/money/phpbench.json with PHP version 8.0.3, xdebug ❌, opcache ✔ \Benchmark\Money\MoneyOperationBench benchAvg................................R1 I63 - [Mo399.867μs vs Mo404.221μs] -1.08% (±2.29%) Subjects: 1, Assertions: 0, Failures: 0, Errors: 0 ```
…eading from `bcadd()` and `bcsub()` operations
…er#__construct()` code, adjusted ctor parameter types
…t)` comparison to see if two strings are equivalent This simplifies checking for the validity of `Number` input values
…stead of a generator Ref: moneyphp#634 (comment)
0ca2933
to
8e0630f
Compare
@frederikbosch should be OK for another round |
How about merging in? It's so huge that I have to work with it in order to find improvements. |
Given the previous review, I suggest reviewing the individual (new) commits one-by-one, since I try to keep commit messages as relevant as possible. |
@frederikbosch just realized that yours may be a typo in the message: from my PoV, this is mergeable, and I have decent confidence in the amount of checks I've added here to avoid major regressions. Minor regressions are unavoidable, but that's normality in OSS: https://xkcd.com/1172/ |
I'm releasing this on a fork to internally test it on a customer project: IMO this is mergeable here too 👍 |
Merged, @Ocramius thanks a lot for all your work here. It is much appreciated by us maintainers, but I guess also by many users. I will try my best to come up with a release - first alpha/beta - as soon as possible. |
@Ocramius You added |
How did it reach it? What was the change? Was there a dependency change? |
I added |
@frederikbosch do you have a diff somewhere? Perhaps |
Yes. |
@frederikbosch that bosh does a general Newer versions of My suggestion is to:
If you need any help with it, I can probably hop on a call this afternoon CEST. |
|
I'm optimizing an application that deals with lots of MoneyPHP instances and operations (50k+ instances). The MoneyPHP instantiation and operations sum up to a few hundred milliseconds for a web request so we needed to optimize that. With the benchmarks provided by @Ocramius I discovered that the To get the most performance in our application I wrote a delegating calculator that uses both implementations. /**
* Optimized `Money\Calculator` that delegates to either
* `BcMathCalculator` or `GmpCalculator` depending on the operation.
*
* `GmpCalculator` is approx 25% faster for add and subtract.
* `BcMathCalculator` is faster for all decimal operations.
*/
class DelegatingCalculator implements Calculator
{
public static function compare(string $a, string $b): int
{
return BcMathCalculator::compare($a, $b);
}
public static function add(string $amount, string $addend): string
{
return GmpCalculator::add($amount, $addend);
}
[...]
Money::registerCalculator(DelegatingCalculator::class); Sorry for responding in this old PR but maybe some one else finds this useful. |
Do you have some benchmarks? Maybe create a PR with a |
This is a draft: still playing around with it.
Ultimately, I'm mostly toying around with the library, removing bits that can be removed without substantial BC Breaks (yes, there will be BC breaks) and inlining code that can be inlined (yes, the code will be inlined).
I suggest avoiding an immediate code review until this is ready and I have stable performance results (most of the benchmarks were performed while my PC was under high load, so the values in the commit messages are not reliable).
In addition to all this, I still need to check if the GMP calculator can be used instead of BcMath, in order of priority, since GMP seems to generally be a bit quicker.
Notable improvements:
vimeo/psalm
)roave/infection-static-analysis-plugin
)Money
,Number
andCurrency
Calculator
discovery by-default (you want a custom calculator? configure it manually!)Benchmark results
CI check status:
Composer Normalize (pull_request) - Failing after 19s - somehow didn't check my updated🤔composer.json
?Fixes
moneyphp/money
declares compatibility with PHP >5.6, but cannot guarantee compatibility with PHP 9 or later #633 (PHP 8 compat is now declared with a stricter range)null
amount)1.0.0
)"5000\n"
is NOT anumeric-string
, so that value is now considered illegal at type level)Calculator
andNumber
are now internal)Money#allocate()
is unsupported)Money
now usenumeric-string
andBcMathCalculator
by default)