I'm a former C# programmer, updating my rusty C++98 (perhaps I should try Rust instead) to modern C++20. My main goal is to get used to C++ standard library types and algorithms.
I chose to solve a palindrome quiz, finding the largest palindrome when multiplying two N-digit numbers. Basically it's a harder version of this puzzle.
Solving this task was no problem for N <=4, but I needed to tweak performance to run in acceptable time for 5 <= N <= 7. For that I needed to
- start at the upper limit to find large numbers quickly and count backwards
- quit the innermost loop early (
break;
) - update the lower limit (
lower_bound
)
With this performance improvement, I can no longer imagine how to use a C++ algorithm instead of the plain for-loops.
Request A: can you help me use a C++ algorithm instead of the innermost "plain old" for-loops?
There's something else: when I implemented the mathematical approach for the palindrome check (is_palindrome_math()
) I first had a bug. With that bug, execution time was ~120 ms only. After fixing the bug (uncomment the line which says bugfix), it takes ~1200 ms (factor 10).
Request B: can you help me apply the bugfix without getting a 10x performance loss?
Request C: just a code review ;-) I used east const, just to see how that feels. Please don't argue for west const. I know the discussion.
I'm using Visual Studio 2022, C++20. Do yourself a favor and run it in Release Build without attaching a debugger for higher N.
#include <algorithm> // std::sort()
#include <cassert> // assert()
#include <chrono> // time measurements
#include <iostream> // std::cout
#include <string> // std::to_string()
#include <vector> // std::vector<T>
#include <tuple> // std::tuple<T>
#include <ranges> // std::ranges::views
#include <map> // std::map<T>
/**
* \brief Checks for palindromes using string and reverse string.
* \param n Number to check being palindrome.
* \return True if the number is a palindrome.
*/
inline bool is_palindrome_str(unsigned long long n)
{
auto const num = std::to_string(n);
auto backward(num);
std::ranges::reverse(backward);
return num == backward;
}
/**
* \brief Checks for palindromes using a string and a forward and reverse iterator.
* \param n Number to check being palindrome.
* \return True if the number is a palindrome.
*/
inline bool is_palindrome_iterator(unsigned long long n)
{
auto const num = std::to_string(n);
auto left = num.begin();
auto right = num.rbegin();
while (std::distance(left, right.base() - 1) > 0)
{
if (*left != *right) return false;
++left;
++right;
}
return true;
}
/**
* \brief Checks for palindromes using a string and accessing individual characters.
* \param n Number to check being palindrome.
* \return True if the number is a palindrome.
*/
inline bool is_palindrome_char(unsigned long long n)
{
auto const num = std::to_string(n);
auto const size = num.size();
for (auto i = 0ULL; i < size / 2; i++)
{
if (num[i] != num[size - i - 1]) return false;
}
return true;
}
/**
* \brief Checks for palindromes using a string and std::all_of for individual characters.
* \param n Number to check being palindrome.
* \return True if the number is a palindrome.
*/
inline bool is_palindrome_allof(unsigned long long n)
{
auto const num = std::to_string(n);
auto const size = num.size();
auto const indexes = std::ranges::views::iota(0ULL, num.size() / 2);
return std::all_of(indexes.begin(), indexes.end(),
[&num, &size](auto index) { return num[index] == num[size - 1 - index]; }
);
}
/**
* \brief Checks for palindromes using math.
* \param n Number to check being palindrome.
* \return True if the number is a palindrome.
*/
inline bool is_palindrome_math(unsigned long long n)
{
// Bugfix:
//if (n % 10 == 0) return false;
auto m = 0ULL;
while (n > m)
{
auto const last_digit = n % 10;
n /= 10;
if (n == m) return true; // exit for odd number of digits
m *= 10;
m += last_digit;
if (n == m) return true; // exit for even number of digits
}
return false;
}
/**
* \brief Calculates the start number and end number of the decade with the number of digits given.
* \param digits Number of digits
* \return Start and end value, e.g. 1000 and 9999 for the 4 digits decade
*/
std::tuple<unsigned int, unsigned int> get_decade_range(unsigned int const digits)
{
unsigned int begin = 1;
for (unsigned int i = 1; i < digits; i++) begin *= 10;
unsigned int end = begin * 10 - 1;
return std::make_tuple(begin, end);
}
/**
* \brief Stopwatch for measuring wall clock time of a method.
* Starts measuring when created, stops measuring when it goes out of scope.
*/
class Timer
{
private:
std::chrono::time_point<std::chrono::steady_clock> const startTime = std::chrono::steady_clock::now();
public:
~Timer()
{
auto const endTime = std::chrono::steady_clock::now();
std::cout << "Calculation took " << std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count() << " ms." << std::endl;
}
};
int main()
{
// Some poor man's unit tests
assert(is_palindrome_char(9234567654329));
assert(is_palindrome_str(9234567654329));
assert(is_palindrome_iterator(9234567654329));
assert(is_palindrome_allof(9234567654329));
assert(is_palindrome_math(9234567654329));
// Different approaches for performance comparison
std::map<std::string, bool (*)(unsigned long long)> palindrome_functions;
palindrome_functions.emplace("math", is_palindrome_math);
palindrome_functions.emplace("char", is_palindrome_char);
palindrome_functions.emplace("str", is_palindrome_str);
palindrome_functions.emplace("iterator", is_palindrome_iterator);
palindrome_functions.emplace("allof", is_palindrome_allof);
auto [begin, end] = get_decade_range(7); // Note to self: this is C++17 structured binding == Python tuple unpacking
std::cout << "Looking for palindromes from " << begin << " to " << end << "\n";
for (auto const& is_palindrome_f : palindrome_functions) // Note to self: we call this a range-based for-loop
{
std::cout << "\nRunning algorithm " << is_palindrome_f.first << "\n";
const Timer _performance;
unsigned long long lower_bound = begin;
std::vector<std::tuple<unsigned long long, unsigned long long, unsigned long long>> palindromes;
// Question: can I use an algorithm here?
for (unsigned long long factor1 = end; factor1 >= lower_bound; factor1--)
{
// Question: or here?
for (unsigned long long factor2 = factor1; factor2 >= lower_bound; factor2--)
{
auto const product = factor1 * factor2;
if (is_palindrome_f.second(product))
{
lower_bound = std::max(factor2, lower_bound);
palindromes.emplace_back(product, factor1, factor2);
break;
}
}
}
auto const highest = *std::ranges::max_element(palindromes);
std::cout << std::get<0>(highest) << "=" << std::get<1>(highest) << "*" << std::get<2>(highest) << "\n";
}
}
Output on my AMD Ryzen 7 2700X, 32 GB RAM (for comparison), /O2
optimization (MSVC does not have /O3
as it seems).
Looking for palindromes from 1000000 to 9999999
Running algorithm allof
99956644665999=9998017*9997647
Calculation took 5310 ms.
Running algorithm char
99956644665999=9998017*9997647
Calculation took 5332 ms.
Running algorithm iterator
99956644665999=9998017*9997647
Calculation took 5195 ms.
Running algorithm math
99956644665999=9998017*9997647
Calculation took 121 ms. <-- bug version
Running algorithm str
99956644665999=9998017*9997647
Calculation took 8113 ms.
palindromes
). It's pure dumb luck this didn't change the result. Remember: slow but correct is always better than fast and wrong! \$\endgroup\$