feat: optimized isbn number calculations

This commit is contained in:
Krzysztof kuhy Rudnicki 2026-02-22 22:19:28 +01:00
parent cc8cc53dfb
commit 21c9180b00
5 changed files with 260 additions and 22 deletions

View File

@ -3,6 +3,9 @@
#include <iostream>
#include <string>
#include <vector>
#ifdef _OPENMP
#include <omp.h>
#endif
#ifndef CHECK_ISBN_CPP
#define CHECK_ISBN_CPP
@ -68,35 +71,111 @@ bool checkISBN(const std::vector<int> isbn) {
return !(sum % CHECK_NUMBER);
}
std::vector<int> intToVector(unsigned long long int number) {
std::vector<int> numbers;
while (number > 0) {
numbers.push_back(number % 10);
number /= 10;
}
std::reverse(numbers.begin(), numbers.end());
// Optimisation 1 no heap allocation: intToVector() removed; checkAll()
// uses only stack variables in the nested loops below.
//
// Optimisation 2 algorithmic reduction (10^10 → 10^9 iterations):
// ISBN-10 validity: 10*d0 + 9*d1 + ... + 2*d8 + 1*d9 ≡ 0 (mod 11)
// Given a 9-digit prefix d0..d8 the check digit is determined:
// d9 = (11 - S%11) % 11 where S = weighted sum of prefix
// The number is valid iff d9 ≤ 9 (i.e. d9 ≠ 10).
//
// Optimisation 3 incremental partial sums + OpenMP:
// Each loop level accumulates its contribution once, not inside the
// innermost body. The outermost loop is parallelised with OpenMP.
//
// Optimisation 4 separated I/O:
// File writing is done in a second serial pass with a large buffer so
// it doesn't contend with (or race against) the parallel counting pass.
long long countISBNs() {
long long sum = 0;
return numbers;
}
int checkAll() {
int sum = 0;
std::ofstream file;
file.open("ISBN.txt");
for (unsigned long long int i = HIGHEST_ISBN; i >= 1; i--) {
// if(DEBUG) std::cout << i << std::endl;
if (checkISBN(intToVector(i))) {
++sum;
file << std::to_string(i) << "\n";
#ifdef _OPENMP
#pragma omp parallel for reduction(+ : sum) schedule(static)
#endif
for (int d0 = 0; d0 <= 9; d0++) {
int s0 = 10 * d0;
for (int d1 = 0; d1 <= 9; d1++) {
int s1 = s0 + 9 * d1;
for (int d2 = 0; d2 <= 9; d2++) {
int s2 = s1 + 8 * d2;
for (int d3 = 0; d3 <= 9; d3++) {
int s3 = s2 + 7 * d3;
for (int d4 = 0; d4 <= 9; d4++) {
int s4 = s3 + 6 * d4;
for (int d5 = 0; d5 <= 9; d5++) {
int s5 = s4 + 5 * d5;
for (int d6 = 0; d6 <= 9; d6++) {
int s6 = s5 + 4 * d6;
for (int d7 = 0; d7 <= 9; d7++) {
int s7 = s6 + 3 * d7;
for (int d8 = 0; d8 <= 9; d8++) {
int d9 = (11 - (s7 + 2 * d8) % 11) % 11;
if (d9 <= 9)
++sum;
}
}
}
}
}
}
}
}
}
file << "There are " << sum << " valid ISBN numbers\n";
file.close();
return sum;
}
void writeISBNsToFile() {
static const int BUF = 1 << 20; // 1 MB write buffer
std::ofstream file;
file.rdbuf()->pubsetbuf(nullptr, BUF);
file.open("ISBN.txt");
long long written = 0;
for (int d0 = 0; d0 <= 9; d0++) {
int s0 = 10 * d0;
for (int d1 = 0; d1 <= 9; d1++) {
int s1 = s0 + 9 * d1;
for (int d2 = 0; d2 <= 9; d2++) {
int s2 = s1 + 8 * d2;
for (int d3 = 0; d3 <= 9; d3++) {
int s3 = s2 + 7 * d3;
for (int d4 = 0; d4 <= 9; d4++) {
int s4 = s3 + 6 * d4;
for (int d5 = 0; d5 <= 9; d5++) {
int s5 = s4 + 5 * d5;
for (int d6 = 0; d6 <= 9; d6++) {
int s6 = s5 + 4 * d6;
for (int d7 = 0; d7 <= 9; d7++) {
int s7 = s6 + 3 * d7;
for (int d8 = 0; d8 <= 9; d8++) {
int s = s7 + 2 * d8;
int d9 = (11 - s % 11) % 11;
if (d9 <= 9) {
long long isbn = (long long)d0 * 1000000000LL +
d1 * 100000000 + d2 * 10000000 +
d3 * 1000000 + d4 * 100000 + d5 * 10000 +
d6 * 1000 + d7 * 100 + d8 * 10 + d9;
file << isbn << '\n';
++written;
}
}
}
}
}
}
}
}
}
}
file << "There are " << written << " valid ISBN numbers\n";
file.close();
}
int main() {
checkAll();
long long count = countISBNs();
std::cout << "There are " << count << " valid ISBN numbers\n";
writeISBNsToFile();
return 0;
}

Binary file not shown.

View File

@ -0,0 +1,90 @@
// Optimized ISBN-10 counter.
//
// Three improvements over the original:
//
// 1. NO HEAP ALLOCATION — original calls intToVector() on every iteration,
// which does a heap alloc+free (std::vector) for each of 10^10 numbers.
// Here we use only stack variables.
//
// 2. ALGORITHMIC REDUCTION — ISBN-10 validity condition:
// 10*d0 + 9*d1 + ... + 2*d8 + 1*d9 ≡ 0 (mod 11)
// Given any 9-digit prefix d0..d8 we can solve for d9 directly:
// d9 = (11 - S mod 11) mod 11 where S = sum of weighted prefix
// The number is valid iff 0 ≤ d9 ≤ 9 (i.e. d9 ≠ 10).
// → Only 10^9 outer iterations instead of 10^10.
//
// 3. INCREMENTAL WEIGHTED SUM — instead of multiplying each digit by its
// weight inside the innermost loop, partial sums are maintained
// incrementally as digits change, so the innermost body is almost free.
// The compiler can also vectorise the innermost digit loop.
//
// Compile: g++ -O2 -fopenmp bench_optimized.cpp -o bench_opt
// (OpenMP is optional; remove -fopenmp if not available it just runs
// the outer loop on a single thread.)
#include <chrono>
#include <iostream>
#ifdef _OPENMP
#include <omp.h>
#endif
int main() {
auto start = std::chrono::high_resolution_clock::now();
long long count = 0;
// Nested loops over the 9 prefix digits.
// s_k = partial weighted sum up through digit k.
// weight for digit at position i (0-based, MSB first) = 10 - i
// so outer digit d0 has weight 10, d1 has weight 9, ..., d8 has
// weight 2.
#ifdef _OPENMP
#pragma omp parallel for reduction(+ : count) schedule(static)
#endif
for (int d0 = 0; d0 <= 9; d0++) {
int s0 = 10 * d0;
for (int d1 = 0; d1 <= 9; d1++) {
int s1 = s0 + 9 * d1;
for (int d2 = 0; d2 <= 9; d2++) {
int s2 = s1 + 8 * d2;
for (int d3 = 0; d3 <= 9; d3++) {
int s3 = s2 + 7 * d3;
for (int d4 = 0; d4 <= 9; d4++) {
int s4 = s3 + 6 * d4;
for (int d5 = 0; d5 <= 9; d5++) {
int s5 = s4 + 5 * d5;
for (int d6 = 0; d6 <= 9; d6++) {
int s6 = s5 + 4 * d6;
for (int d7 = 0; d7 <= 9; d7++) {
int s7 = s6 + 3 * d7;
// Innermost: vary d8, weight 2.
for (int d8 = 0; d8 <= 9; d8++) {
int s = s7 + 2 * d8;
// Required check digit (weight 1):
int d9 = (11 - s % 11) % 11;
if (d9 <= 9)
++count;
}
}
}
}
}
}
}
}
}
auto end = std::chrono::high_resolution_clock::now();
double elapsed = std::chrono::duration<double>(end - start).count();
std::cout << "Valid ISBNs: " << count << "\n";
std::cout << "Time: " << elapsed << " s\n";
#ifdef _OPENMP
std::cout << "Threads: " << omp_get_max_threads() << "\n";
#endif
return 0;
}

Binary file not shown.

View File

@ -0,0 +1,69 @@
// Benchmark version of the original algorithm.
// File I/O removed so we measure pure computation time.
#include <algorithm>
#include <chrono>
#include <iostream>
#include <vector>
const int ISBN_LENGTH = 10;
const int CHECK_NUMBER = 11;
const unsigned long long int HIGHEST_ISBN = 9999999999ULL;
bool checkISBN(const std::vector<int> isbn) {
int sum = 0, t = 0;
for (int i = 0; i < ISBN_LENGTH; i++) {
t += isbn[i];
sum += t;
}
return !(sum % CHECK_NUMBER);
}
std::vector<int> intToVector(unsigned long long int number) {
std::vector<int> numbers;
while (number > 0) {
numbers.push_back(number % 10);
number /= 10;
}
std::reverse(numbers.begin(), numbers.end());
return numbers;
}
// Run for at most SAMPLE seconds, then extrapolate total time.
static constexpr double SAMPLE_SECS = 20.0;
long long checkAllTimed(double &elapsed_out) {
auto start = std::chrono::high_resolution_clock::now();
auto limit = start + std::chrono::duration<double>(SAMPLE_SECS);
long long sum = 0;
unsigned long long i;
for (i = HIGHEST_ISBN; i >= 1; i--) {
if (checkISBN(intToVector(i)))
++sum;
// Check wall-clock every 1 million iterations to keep overhead low.
if ((i & 0xFFFFF) == 0) {
if (std::chrono::high_resolution_clock::now() >= limit)
break;
}
}
auto end = std::chrono::high_resolution_clock::now();
elapsed_out = std::chrono::duration<double>(end - start).count();
unsigned long long done = HIGHEST_ISBN - i;
double rate = (double)done / elapsed_out; // numbers/s
double total_est = (double)HIGHEST_ISBN / rate;
std::cout << "Iterated: " << done << " numbers in " << elapsed_out
<< " s\n";
std::cout << "Rate: " << (long long)rate << " numbers/s\n";
std::cout << "Estimated total time for full range: " << (long long)total_est
<< " s (" << total_est / 60.0 << " min)\n";
return sum;
}
int main() {
double elapsed = 0.0;
long long count = checkAllTimed(elapsed);
std::cout << "Valid ISBNs in sampled range: " << count << "\n";
return 0;
}