-
Notifications
You must be signed in to change notification settings - Fork 385
Integer Powers Unraveled
Logarithms and powers (exponents) are occasionally useful in Bitcoin work, especially base (radix) 2. But C++ log functions are all floating point. There is not much call for floating point calculation in Bitcoin, and using the wrong data types causes code bloat due to type casting, and compiler warnings otherwise. It can also result in logic errors due to unexpected rounding behavior. Floating point operations may also be more CPU intensive than those necessary for integer operations. So we cleaned up some code by implementing a proper suite of integer exponentiation functions.
Natural logarithms are inherently floating point (base e) and logarithms of negative numbers are non-continuous functions, so implementation of these is a non-objective.
- Provide power and log, for all integer types.
- Provide simplified base 2 variants of power and log.
- log2(n) identical in behavior to log(2, n).
- power2(n) identical in behavior to power(2, n).
- Provide both ceilinged and floored logarithm variants.
- Allow result type specification with appropriate defaults.
- Maintain overflow behavior of native operators.
- Return a common sentinel for undefined operations.
- Avoid unnecessary computation.
Integer powers are implemented as repeated multiplication of the base by itself.
Base 2 power is repeated multiplication by 2, so the left shift operator (<<
) may be used as a performance optimization. While the compiler may optimize for multiplication by 2 at run time, compiling for it explicitly precludes at least one condition and branch in the object code.
All integer power parameters are defined with the exception of power(0, 0)
. Apart from an overflow, 0 is the valid result only for any power of 0. So 0 is used as the invalid parameter sentinel for consistency with the logarithm functions. As with all native operators, overflow guards are left to the caller. The implementation may not cause an overflow that is not inherent in the parameterization.
The result type is the value type, as the value is multiplied by itself.
Integer logarithms are implemented as repeated division of the value by the base. Native C++ division is truncated. However logarithms allow only positive value and base. Truncation of a positive quotient is floored, so only floored and ceilinged functions are required to support the three common rounding methods.
Base 2 logarithm is repeated division by 2, so the right shift operator (>>
) may be used as a performance optimization. While the compiler may optimize for division by 2 at run time, compiling for it explicitly precludes at least one condition and branch in the object code.
The logarithm of a base less than 2 or a value less than 1 is undefined. Apart from an overflow, there is no valid 0 result. So 0 is used as the invalid parameter sentinel. As with all native operators, overflow guards are left to the caller. The implementation may not cause an overflow that is not inherent in the parameterization.
Iteration counting implies that the the result type is a non-negative arbitrary integer.
These templates determine the odd-ness, sign, and absolute value of a signed or unsigned integer type.
#include <type_traits>
template <typename Integer, if_integer<Integer> = true>
constexpr bool is_odd(Integer value) noexcept
{
return (value % 2) != 0;
}
template <typename Integer, if_signed_integer<Integer> = true>
constexpr bool is_negative(Integer value) noexcept
{
return value < 0;
}
template <typename Integer, if_unsigned_integer<Integer> = true>
constexpr bool is_negative(Integer value) noexcept
{
return false;
}
template <typename Integer,
typename Absolute = std::make_unsigned<Integer>::type,
if_signed_integer<Integer> = true>
constexpr Absolute absolute(Integer value) noexcept
{
return is_negative(value) ? -value : value;
}
template <typename Integer,
typename Absolute = Integer,
if_unsigned_integer<Integer> = true>
constexpr Absolute absolute(Integer value) noexcept
{
return value;
}
These templates implement the power functions.
#include <cstddef>
// Returns 0 for undefined (0, 0).
template <typename Value = size_t, typename Base, typename Exponent,
if_integer<Value> = true, if_integer<Base> = true, if_integer<Exponent> = true>
inline Value power(Base base, Exponent exponent) noexcept
{
if (base == 0)
return 0;
if (exponent == 0)
return 1;
if (is_negative(exponent))
return absolute(base) > 1 ? 0 :
(is_odd(exponent) && is_negative(base) ? -1 : 1);
Value value = base;
while (--exponent > 0) { value *= base; }
return value;
}
template <typename Value = size_t, typename Exponent,
if_integer<Value> = true, if_integer<Exponent> = true>
inline Value power2(Exponent exponent) noexcept
{
if (exponent == 0)
return 1;
if (is_negative(exponent))
return 0;
Value value = 2;
while (--exponent > 0) { value <<= 1; };
return value;
}
These templates implement the logarithm functions.
#include <cstddef>
// Returns 0 for undefined (base < 2 or value < 1).
template <typename Exponent = size_t, typename Base, typename Value,
if_integer<Exponent> = true, if_integer<Base> = true, if_integer<Value> = true>
inline Exponent ceilinged_log(Base base, Value value) noexcept
{
if (base < 2 || value < 1)
return 0;
Exponent exponent = 0;
while (value > 0) { ++exponent; value /= base; }
return exponent;
}
// Returns 0 for undefined (value < 1).
template <typename Exponent = size_t, typename Value,
if_integer<Exponent> = true, if_integer<Value> = true>
inline Exponent ceilinged_log2(Value value) noexcept
{
if (is_negative(value))
return 0;
// C++ 20 (std::bit_width).
using exponent = std::make_unsigned<Exponent>::type;
const auto from = static_cast<exponent>(value);
return static_cast<Exponent>(std::bit_width(from));
}
// Returns 0 for undefined (base < 2 or value < 1).
template <typename Exponent = size_t, typename Base, typename Value,
if_integer<Exponent> = true, if_integer<Base> = true, if_integer<Value> = true>
inline Exponent floored_log(Base base, Value value) noexcept
{
if (base < 2 || value < 1)
return 0;
Exponent exponent = 0;
while (((value /= base)) > 0) { ++exponent; }
return exponent;
}
// Returns 0 for undefined (value < 1).
template <typename Exponent = size_t, typename Value,
if_integer<Exponent> = true, if_integer<Value> = true>
inline Exponent floored_log2(Value value) noexcept
{
if (value < 1)
return 0;
// C++ 20 (std::bit_width).
using exponent = std::make_unsigned<Exponent>::type;
const auto from = static_cast<exponent>(value);
return static_cast<Exponent>(std::bit_width(from) + 1);
}
The use of std::signbit
is avoided as it casts to double
, though otherwise would be sufficient to replace the is_negative
templates.
The inline
keyword advises the compiler that inlining of the functions is preferred. This removes call stack overhead, assuming the compiler respects the request. Generally we prefer to let the compiler make these decisions, preserving code readability.
A compiler may warn (incorrectly) of division by zero "possibility" in a logarithm base
const
0 test case, given that it is inlining (unreachable) division by literal 0. Removal of theinline
keyword can prevent this if desired, but the warning is beneficial for production (vs. test) code. A better alternative may be to use a non-const, zero-valued base variable in the logarithm test cases.
The constexpr
keyword ensures that functions can be evaluated at compile time. In C++14 the inlined functions above can also be changed to constexpr
. This also allows the functions to be integrated into template type constraints and static_assert
expressions.
The following section of power
, and the corresponding but reduced section of power2
, implement three short-circuits that also serve as necessary guards for the while
loops.
if (base == 0)
return 0;
if (exponent == 0)
return 1;
if (is_negative(exponent))
return absolute(base) > 1 ? 0 :
(is_odd(exponent) && is_negative(base) ? -1 : 1);
The base == 0
guard takes advantage of the fact that 0 to any power is 0, while also serving up the sentinel value for the power(0, 0)
undefined condition. The exponent == 0
guard takes advantage of the fact that any value to the power of 0 is 1. The is_negative(exponent)
guard takes advantage of the fact that any value to a negative power is between -1 and +1 (inclusive). Given integer operations, this is limited to { -1, 0, +1 }.
In each of the logarithm functions, there is a base < 2
(as applicable) and value < 1
condition. These are not optimizations, as they only determine parameter validity, but they do also serve as necessary guards for the while
loops.
Both power2(n)
and floored_log2(n)
could simply call power(2, n)
and floored_log(2, n)
respectively. The implementations are nearly identical, with the exception of shift left or shift right vs. multiply by 2 or divide by 2. However there are optimizations in removing both unnecessary guards and the presumed run-time condition and branch to a shifted optimization.
Despite the relative verbosity of the templates the result should be as optimal as manually inlining the minimal necessary operations. A few runs through an NDEBUG build in a debugger confirm this.
Without the template overloads there would be warnings on unsigned type power
operands, as they all invoke value < 0
, which is always false
(tautological). It is trivial to replace all of these calls with value < 1
calls, as in each case 0
has been excluded. However, this materially impacts readability and there is no performance benefit. In fact, the inlined templates compile away for unsigned types whereas a < 1
condition might not, so there is a potential performance advantage. For functions or operand combinations that are not referenced, the corresponding templates are not even compiled. The use of std::abs
avoided as it is limited to signed types.
Template specialization could be further employed to reduce a couple calls when parameters are unsigned. However there is little to no actual performance optimization and the denormalization didn't seem like a worthwhile compromise.
The templates can be factored into header (.hpp) and implementation (.ipp) files, just be sure to remove the default template parameter values in the implementation.
See Type Constraints Unraveled for an explanation of the template type constraints used above.
As the power result is always non-negative, the value type is defaulted to size_t
. Ideally for powers it would default to Base, which may be unsigned and/or non-integral. However this would complicate providing a simple overridable default, as Value would have to follow Base in the template parameter list. As size_t
is generally the largest integral type this is a reasonable default for all integral parameters, and can be overridden with a single template parameter as desired. The logarithm result defaults to size_t
as it is a count, not based on any parameter.
Exponents and logarithms are inverse functions, so these relations must hold for all defined { b, n }, excepting overflows.
-
floored_log(b, power(b, n)) == n
. -
ceilinged_log(b, power(b, n)) == n
.
- Behavior satisfies the inverse relation for all sign combinations.
- Result type may be specified and defaults rationally.
- Behavior is consistent with native operators.
- The functions cannot cause overflows.
- There can be no "tautological compare" warnings from unsigned parameters.
- The functions return a common sentinel for undefined operations.
- Stack calls may be fully removed by inlining.
- All functions can be
constexpr
in C++14. - The
is_negative
andabsolute
calls compile away forunsigned
operators. - The
is_odd
,is_negative
, andabsolute
calls compile away forunsigned
andconstexpr
parameters. - All conditions compile away when the above render them tautological.
- All that remains is necessary.
Users | Developers | License | Copyright © 2011-2024 libbitcoin developers
- Home
- manifesto
- libbitcoin.info
- Libbitcoin Institute
- Freenode (IRC)
- Mailing List
- Slack Channel
- Build Libbitcoin
- Comprehensive Overview
- Developer Documentation
- Tutorials (aaronjaramillo)
- Bitcoin Unraveled
-
Cryptoeconomics
- Foreword by Amir Taaki
- Value Proposition
- Axiom of Resistance
- Money Taxonomy
- Pure Bank
- Production and Consumption
- Labor and Leisure
- Custodial Risk Principle
- Dedicated Cost Principle
- Depreciation Principle
- Expression Principle
- Inflation Principle
- Other Means Principle
- Patent Resistance Principle
- Risk Sharing Principle
- Reservation Principle
- Scalability Principle
- Subjective Inflation Principle
- Consolidation Principle
- Fragmentation Principle
- Permissionless Principle
- Public Data Principle
- Social Network Principle
- State Banking Principle
- Substitution Principle
- Cryptodynamic Principles
- Censorship Resistance Property
- Consensus Property
- Stability Property
- Utility Threshold Property
- Zero Sum Property
- Threat Level Paradox
- Miner Business Model
- Qualitative Security Model
- Proximity Premium Flaw
- Variance Discount Flaw
- Centralization Risk
- Pooling Pressure Risk
- ASIC Monopoly Fallacy
- Auditability Fallacy
- Balance of Power Fallacy
- Blockchain Fallacy
- Byproduct Mining Fallacy
- Causation Fallacy
- Cockroach Fallacy
- Credit Expansion Fallacy
- Debt Loop Fallacy
- Decoupled Mining Fallacy
- Dumping Fallacy
- Empty Block Fallacy
- Energy Exhaustion Fallacy
- Energy Store Fallacy
- Energy Waste Fallacy
- Fee Recovery Fallacy
- Genetic Purity Fallacy
- Full Reserve Fallacy
- Halving Fallacy
- Hoarding Fallacy
- Hybrid Mining Fallacy
- Ideal Money Fallacy
- Impotent Mining Fallacy
- Inflation Fallacy
- Inflationary Quality Fallacy
- Jurisdictional Arbitrage Fallacy
- Lunar Fallacy
- Network Effect Fallacy
- Prisoner's Dilemma Fallacy
- Private Key Fallacy
- Proof of Cost Fallacy
- Proof of Memory Façade
- Proof of Stake Fallacy
- Proof of Work Fallacy
- Regression Fallacy
- Relay Fallacy
- Replay Protection Fallacy
- Reserve Currency Fallacy
- Risk Free Return Fallacy
- Scarcity Fallacy
- Selfish Mining Fallacy
- Side Fee Fallacy
- Split Credit Expansion Fallacy
- Stock to Flow Fallacy
- Thin Air Fallacy
- Time Preference Fallacy
- Unlendable Money Fallacy
- Fedcoin Objectives
- Hearn Error
- Collectible Tautology
- Price Estimation
- Savings Relation
- Speculative Consumption
- Spam Misnomer
- Efficiency Paradox
- Split Speculator Dilemma
- Bitcoin Labels
- Brand Arrogation
- Reserve Definition
- Maximalism Definition
- Shitcoin Definition
- Glossary
- Console Applications
- Development Libraries
- Maintainer Information
- Miscellaneous Articles