Skip to content

Instantly share code, notes, and snippets.

@tooruu
Last active March 18, 2024 14:25
Show Gist options
  • Save tooruu/f1d0965c04db3b6f84d814f0be947073 to your computer and use it in GitHub Desktop.
Save tooruu/f1d0965c04db3b6f84d814f0be947073 to your computer and use it in GitHub Desktop.
Number conversion benchmark and validator
<?php
declare(strict_types=1);
abstract readonly class BtcConverterBenchmark
{
abstract protected function sat2Btc(int $num): string;
abstract protected function btc2Sat(string $num): int;
public function __construct(protected int $runs = 1_000_000)
{
}
/**
* @param int|string $num If string then it is BTC amount, otherwise - Satoshi.
* @param int|string|null $expected Optional. If set, check the result against it.
* Must be opposite of $num's type.
* @return float Average time in nanoseconds operation took to perform.
*
* @throws ValueError if $expected is set, and it does not strictly equal the result.
*/
final public function run(int|string $num, int|string|null $expected = null): float
{
if (gettype($num) === gettype($expected)) {
throw new TypeError('$num and $assert can\'t be the same type');
}
$worker = is_int($num) ? $this->sat2Btc(...) : $this->btc2Sat(...);
$start = hrtime(true);
$result = $worker($num);
$end = hrtime(true);
if ($expected && $expected !== $result) {
throw $this->formatError($num, $expected, $result);
}
$sum = array_reduce(
range(2, $this->runs),
function (int $carry, int $ignored) use ($worker, $num): int {
$start = hrtime(true);
$worker($num);
$end = hrtime(true);
return $carry + $end - $start;
},
$end - $start,
);
return $sum / $this->runs;
}
protected function formatError(int|string $number, string|int $expected, mixed $actual): ValueError
{
if (is_int($number)) {
$number = rtrim(rtrim(number_format($number, 8, '.', '_'), '0'), '.');
}
return new ValueError(
PHP_EOL.'given: '.gettype($number)." $number".
PHP_EOL.'want: '.gettype($expected)." $expected".
PHP_EOL.'got: '.gettype($actual).' '.print_r($actual, true).
PHP_EOL
);
}
}
readonly class GMPBenchmark extends BtcConverterBenchmark
{
protected GMP $BTC_PRECISION;
public function __construct(...$args)
{
parent::__construct(...$args);
$this->BTC_PRECISION = gmp_init(10 ** 8);
}
protected function sat2Btc(int $num): string
{
// Leading zeros in remainder are cut off, so this doesn't pass tests.
return rtrim(rtrim(implode('.', gmp_div_qr($num, $this->BTC_PRECISION)), '0'), '.');
}
protected function btc2Sat(string $num): int
{
// Only works with integer strings.
return gmp_intval(gmp_mul($num, 10 ** 8));
}
}
readonly class BCMathBenchmark extends BtcConverterBenchmark
{
protected const string BTC_PRECISION = '100000000';
protected function sat2Btc(int $num): string
{
return rtrim(rtrim(bcdiv((string) $num, self::BTC_PRECISION, 8), '0'), '.');
}
protected function btc2Sat(string $num): int
{
return (int) bcmul((string) $num, self::BTC_PRECISION);
}
}
readonly class NativeBenchmark extends BtcConverterBenchmark
{
protected const int BTC_PRECISION = 10 ** 8;
protected function sat2Btc(int $num): string
{
return rtrim(rtrim(number_format($num / self::BTC_PRECISION, 8, '.', ''), '0'), '.');
}
protected function btc2Sat(string $num): int
{
return intval(strval($num * self::BTC_PRECISION));
}
}
function runBenchmarks(int|string $amount, int|string|null $expected = null): void
{
global $gmp, $bc, $native;
//echo 'gmp: '.$gmp->run($amount, $expected).PHP_EOL;
echo 'bc: '.$bc->run($amount, $expected).PHP_EOL;
echo 'native: '.$native->run($amount, $expected).PHP_EOL;
echo PHP_EOL;
}
[$gmp, $bc, $native] = [new GMPBenchmark, new BCMathBenchmark, new NativeBenchmark];
runBenchmarks(amount: 1_000_000_000, expected: '10');
runBenchmarks(amount: '10', expected: 1_000_000_000);
runBenchmarks(amount: 9876, expected: '0.00009876');
runBenchmarks(amount: '0.00009876', expected: 9876);
runBenchmarks(amount: 123456789, expected: '1.23456789');
runBenchmarks(amount: '1.23456789', expected: 123456789);
runBenchmarks(amount: 2_100_000_000_000_000, expected: '21000000');
runBenchmarks(amount: '21000000.00', expected: 2_100_000_000_000_000);
runBenchmarks(amount: '1.0', expected: 100_000_000);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment