// Copyright 2021 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include "src/bigint/bigint-internal.h" #include "src/bigint/util.h" namespace v8 { namespace bigint { namespace test { int PrintHelp(char** argv) { std::cerr << "Usage:\n" << argv[0] << " --help\n" << " Print this help and exit.\n" << argv[0] << " --list\n" << " List supported tests.\n" << argv[0] << " \n" << " Run the specified test (see --list for a list).\n" << "\nOptions when running tests:\n" << "--random-seed R\n" << " Initialize the random number generator with this seed.\n" << "--runs N\n" << " Repeat the test N times.\n"; return 1; } #define TESTS(V) \ V(kBarrett, "barrett") \ V(kBurnikel, "burnikel") \ V(kFFT, "fft") \ V(kFromString, "fromstring") \ V(kFromStringBase2, "fromstring2") \ V(kKaratsuba, "karatsuba") \ V(kToom, "toom") \ V(kToString, "tostring") enum Operation { kNoOp, kList, kTest }; enum Test { #define TEST(kName, name) kName, TESTS(TEST) #undef TEST }; class RNG { public: RNG() = default; void Initialize(int64_t seed) { state0_ = MurmurHash3(static_cast(seed)); state1_ = MurmurHash3(~state0_); CHECK(state0_ != 0 || state1_ != 0); } uint64_t NextUint64() { XorShift128(&state0_, &state1_); return static_cast(state0_ + state1_); } static inline void XorShift128(uint64_t* state0, uint64_t* state1) { uint64_t s1 = *state0; uint64_t s0 = *state1; *state0 = s0; s1 ^= s1 << 23; s1 ^= s1 >> 17; s1 ^= s0; s1 ^= s0 >> 26; *state1 = s1; } static uint64_t MurmurHash3(uint64_t h) { h ^= h >> 33; h *= uint64_t{0xFF51AFD7ED558CCD}; h ^= h >> 33; h *= uint64_t{0xC4CEB9FE1A85EC53}; h ^= h >> 33; return h; } private: uint64_t state0_; uint64_t state1_; }; static constexpr int kCharsPerDigit = kDigitBits / 4; static const char kConversionChars[] = "0123456789abcdefghijklmnopqrstuvwxyz"; std::string FormatHex(Digits X) { X.Normalize(); if (X.len() == 0) return "0"; digit_t msd = X.msd(); const int msd_leading_zeros = CountLeadingZeros(msd); const size_t bit_length = X.len() * kDigitBits - msd_leading_zeros; const size_t chars = DIV_CEIL(bit_length, 4); if (chars > 100000) { return std::string(""); } std::unique_ptr result(new char[chars]); for (size_t i = 0; i < chars; i++) result[i] = '?'; // Print the number into the string, starting from the last position. int pos = static_cast(chars - 1); for (int i = 0; i < X.len() - 1; i++) { digit_t d = X[i]; for (int j = 0; j < kCharsPerDigit; j++) { result[pos--] = kConversionChars[d & 15]; d = static_cast(d >> 4u); } } while (msd != 0) { result[pos--] = kConversionChars[msd & 15]; msd = static_cast(msd >> 4u); } CHECK(pos == -1); return std::string(result.get(), chars); } class Runner { public: Runner() = default; void Initialize() { rng_.Initialize(random_seed_); processor_.reset(Processor::New(new Platform())); } ProcessorImpl* processor() { return static_cast(processor_.get()); } int Run() { if (op_ == kList) { ListTests(); } else if (op_ == kTest) { RunTest(); } else { DCHECK(false); // Unreachable. } return 0; } void ListTests() { #define PRINT(kName, name) std::cout << name << "\n"; TESTS(PRINT) #undef PRINT } void AssertEquals(Digits input1, Digits input2, Digits expected, Digits actual) { if (Compare(expected, actual) == 0) return; std::cerr << "Input 1: " << FormatHex(input1) << "\n"; std::cerr << "Input 2: " << FormatHex(input2) << "\n"; std::cerr << "Expected: " << FormatHex(expected) << "\n"; std::cerr << "Actual: " << FormatHex(actual) << "\n"; error_ = true; } void AssertEquals(Digits X, int radix, char* expected, int expected_length, char* actual, int actual_length) { if (expected_length == actual_length && std::memcmp(expected, actual, actual_length) == 0) { return; } std::cerr << "Input: " << FormatHex(X) << "\n"; std::cerr << "Radix: " << radix << "\n"; std::cerr << "Expected: " << std::string(expected, expected_length) << "\n"; std::cerr << "Actual: " << std::string(actual, actual_length) << "\n"; error_ = true; } void AssertEquals(const char* input, int input_length, int radix, Digits expected, Digits actual) { if (Compare(expected, actual) == 0) return; std::cerr << "Input: " << std::string(input, input_length) << "\n"; std::cerr << "Radix: " << radix << "\n"; std::cerr << "Expected: " << FormatHex(expected) << "\n"; std::cerr << "Actual: " << FormatHex(actual) << "\n"; error_ = true; } int RunTest() { int count = 0; if (test_ == kBarrett) { for (int i = 0; i < runs_; i++) { TestBarrett(&count); } } else if (test_ == kBurnikel) { for (int i = 0; i < runs_; i++) { TestBurnikel(&count); } } else if (test_ == kFFT) { for (int i = 0; i < runs_; i++) { TestFFT(&count); } } else if (test_ == kKaratsuba) { for (int i = 0; i < runs_; i++) { TestKaratsuba(&count); } } else if (test_ == kToom) { for (int i = 0; i < runs_; i++) { TestToom(&count); } } else if (test_ == kToString) { for (int i = 0; i < runs_; i++) { TestToString(&count); } } else if (test_ == kFromString) { for (int i = 0; i < runs_; i++) { TestFromString(&count); } } else if (test_ == kFromStringBase2) { for (int i = 0; i < runs_; i++) { TestFromStringBaseTwo(&count); } } else { DCHECK(false); // Unreachable. } if (error_) return 1; std::cout << count << " tests run, no error reported.\n"; return 0; } void TestKaratsuba(int* count) { // Calling {MultiplyKaratsuba} directly is only valid if // left_size >= right_size and right_size >= kKaratsubaThreshold. constexpr int kMin = kKaratsubaThreshold; constexpr int kMax = 3 * kKaratsubaThreshold; for (int right_size = kMin; right_size <= kMax; right_size++) { for (int left_size = right_size; left_size <= kMax; left_size++) { ScratchDigits A(left_size); ScratchDigits B(right_size); int result_len = MultiplyResultLength(A, B); ScratchDigits result(result_len); ScratchDigits result_schoolbook(result_len); GenerateRandom(A); GenerateRandom(B); processor()->MultiplyKaratsuba(result, A, B); processor()->MultiplySchoolbook(result_schoolbook, A, B); AssertEquals(A, B, result_schoolbook, result); if (error_) return; (*count)++; } } } void TestToom(int* count) { #if V8_ADVANCED_BIGINT_ALGORITHMS // {MultiplyToomCook} works fine even below the threshold, so we can // save some time by starting small. constexpr int kMin = kToomThreshold - 60; constexpr int kMax = kToomThreshold + 10; for (int right_size = kMin; right_size <= kMax; right_size++) { for (int left_size = right_size; left_size <= kMax; left_size++) { ScratchDigits A(left_size); ScratchDigits B(right_size); int result_len = MultiplyResultLength(A, B); ScratchDigits result(result_len); ScratchDigits result_karatsuba(result_len); GenerateRandom(A); GenerateRandom(B); processor()->MultiplyToomCook(result, A, B); // Using Karatsuba as reference. processor()->MultiplyKaratsuba(result_karatsuba, A, B); AssertEquals(A, B, result_karatsuba, result); if (error_) return; (*count)++; } } #endif // V8_ADVANCED_BIGINT_ALGORITHMS } void TestFFT(int* count) { #if V8_ADVANCED_BIGINT_ALGORITHMS // Larger multiplications are slower, so to keep individual runs fast, // we test a few random samples. With build bots running 24/7, we'll // get decent coverage over time. uint64_t random_bits = rng_.NextUint64(); int min = kFftThreshold - static_cast(random_bits & 1023); random_bits >>= 10; int max = kFftThreshold + static_cast(random_bits & 1023); random_bits >>= 10; // If delta is too small, then this run gets too slow. If it happened // to be zero, we'd even loop forever! int delta = 10 + (random_bits & 127); std::cout << "min " << min << " max " << max << " delta " << delta << "\n"; for (int right_size = min; right_size <= max; right_size += delta) { for (int left_size = right_size; left_size <= max; left_size += delta) { ScratchDigits A(left_size); ScratchDigits B(right_size); int result_len = MultiplyResultLength(A, B); ScratchDigits result(result_len); ScratchDigits result_toom(result_len); GenerateRandom(A); GenerateRandom(B); processor()->MultiplyFFT(result, A, B); // Using Toom-Cook as reference. processor()->MultiplyToomCook(result_toom, A, B); AssertEquals(A, B, result_toom, result); if (error_) return; (*count)++; } } #endif // V8_ADVANCED_BIGINT_ALGORITHMS } void TestBurnikel(int* count) { // Start small to save test execution time. constexpr int kMin = kBurnikelThreshold / 2; constexpr int kMax = 2 * kBurnikelThreshold; for (int right_size = kMin; right_size <= kMax; right_size++) { for (int left_size = right_size; left_size <= kMax; left_size++) { ScratchDigits A(left_size); ScratchDigits B(right_size); GenerateRandom(A); GenerateRandom(B); int quotient_len = DivideResultLength(A, B); int remainder_len = right_size; ScratchDigits quotient(quotient_len); ScratchDigits quotient_schoolbook(quotient_len); ScratchDigits remainder(remainder_len); ScratchDigits remainder_schoolbook(remainder_len); processor()->DivideBurnikelZiegler(quotient, remainder, A, B); processor()->DivideSchoolbook(quotient_schoolbook, remainder_schoolbook, A, B); AssertEquals(A, B, quotient_schoolbook, quotient); AssertEquals(A, B, remainder_schoolbook, remainder); if (error_) return; (*count)++; } } } #if V8_ADVANCED_BIGINT_ALGORITHMS void TestBarrett_Internal(int left_size, int right_size) { ScratchDigits A(left_size); ScratchDigits B(right_size); GenerateRandom(A); GenerateRandom(B); int quotient_len = DivideResultLength(A, B); // {DivideResultLength} doesn't expect to be called for sizes below // {kBarrettThreshold} (which we do here to save time), so we have to // manually adjust the allocated result length. if (B.len() < kBarrettThreshold) quotient_len++; int remainder_len = right_size; ScratchDigits quotient(quotient_len); ScratchDigits quotient_burnikel(quotient_len); ScratchDigits remainder(remainder_len); ScratchDigits remainder_burnikel(remainder_len); processor()->DivideBarrett(quotient, remainder, A, B); processor()->DivideBurnikelZiegler(quotient_burnikel, remainder_burnikel, A, B); AssertEquals(A, B, quotient_burnikel, quotient); AssertEquals(A, B, remainder_burnikel, remainder); } void TestBarrett(int* count) { // We pick a range around kBurnikelThreshold (instead of kBarrettThreshold) // to save test execution time. constexpr int kMin = kBurnikelThreshold / 2; constexpr int kMax = 2 * kBurnikelThreshold; // {DivideBarrett(A, B)} requires that A.len > B.len! for (int right_size = kMin; right_size <= kMax; right_size++) { for (int left_size = right_size + 1; left_size <= kMax; left_size++) { TestBarrett_Internal(left_size, right_size); if (error_) return; (*count)++; } } // We also test one random large case. uint64_t random_bits = rng_.NextUint64(); int right_size = kBarrettThreshold + static_cast(random_bits & 0x3FF); random_bits >>= 10; int left_size = right_size + 1 + static_cast(random_bits & 0x3FFF); random_bits >>= 14; TestBarrett_Internal(left_size, right_size); if (error_) return; (*count)++; } #else void TestBarrett(int* count) {} #endif // V8_ADVANCED_BIGINT_ALGORITHMS void TestToString(int* count) { constexpr int kMin = kToStringFastThreshold / 2; constexpr int kMax = kToStringFastThreshold * 2; for (int size = kMin; size < kMax; size++) { ScratchDigits X(size); GenerateRandom(X); for (int radix = 2; radix <= 36; radix++) { int chars_required = ToStringResultLength(X, radix, false); int result_len = chars_required; int reference_len = chars_required; std::unique_ptr result(new char[result_len]); std::unique_ptr reference(new char[reference_len]); processor()->ToStringImpl(result.get(), &result_len, X, radix, false, true); processor()->ToStringImpl(reference.get(), &reference_len, X, radix, false, false); AssertEquals(X, radix, reference.get(), reference_len, result.get(), result_len); if (error_) return; (*count)++; } } } void TestFromString(int* count) { constexpr int kMaxDigits = 1 << 20; // Any large-enough value will do. constexpr int kMin = kFromStringLargeThreshold / 2; constexpr int kMax = kFromStringLargeThreshold * 2; for (int size = kMin; size < kMax; size++) { // To keep test execution times low, test one random radix every time. // Generally, radixes 2 through 36 (inclusive) are supported; however // the functions {FromStringLarge} and {FromStringClassic} can't deal // with the data format that {Parse} creates for power-of-two radixes, // so we skip power-of-two radixes here (and test them separately below). // We round up the number of radixes in the list to 32 by padding with // 10, giving decimal numbers extra test coverage, and making it easy // to evenly map a random number into the index space. constexpr uint8_t radixes[] = {3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 33, 34, 35, 36, 10, 10}; int radix_index = (rng_.NextUint64() & 31); int radix = radixes[radix_index]; int num_chars = std::round(size * kDigitBits / std::log2(radix)); std::unique_ptr chars(new char[num_chars]); GenerateRandomString(chars.get(), num_chars, radix); FromStringAccumulator accumulator(kMaxDigits); FromStringAccumulator ref_accumulator(kMaxDigits); const char* start = chars.get(); const char* end = chars.get() + num_chars; accumulator.Parse(start, end, radix); ref_accumulator.Parse(start, end, radix); ScratchDigits result(accumulator.ResultLength()); ScratchDigits reference(ref_accumulator.ResultLength()); processor()->FromStringLarge(result, &accumulator); processor()->FromStringClassic(reference, &ref_accumulator); AssertEquals(start, num_chars, radix, result, reference); if (error_) return; (*count)++; } } void TestFromStringBaseTwo(int* count) { constexpr int kMaxDigits = 1 << 20; // Any large-enough value will do. constexpr int kMin = 1; constexpr int kMax = 100; for (int size = kMin; size < kMax; size++) { ScratchDigits X(size); GenerateRandom(X); for (int bits = 1; bits <= 5; bits++) { int radix = 1 << bits; int chars_required = ToStringResultLength(X, radix, false); int string_len = chars_required; std::unique_ptr chars(new char[string_len]); processor()->ToStringImpl(chars.get(), &string_len, X, radix, false, true); // Fill any remaining allocated characters with garbage to test that // too. for (int i = string_len; i < chars_required; i++) { chars[i] = '?'; } const char* start = chars.get(); const char* end = start + chars_required; FromStringAccumulator accumulator(kMaxDigits); accumulator.Parse(start, end, radix); ScratchDigits result(accumulator.ResultLength()); processor()->FromString(result, &accumulator); AssertEquals(start, chars_required, radix, X, result); if (error_) return; (*count)++; } } } template bool ParseInt(char* s, I* out) { char* end; if (s[0] == '\0') return false; errno = 0; long l = strtol(s, &end, 10); if (errno != 0 || *end != '\0' || l > std::numeric_limits::max() || l < std::numeric_limits::min()) { return false; } *out = static_cast(l); return true; } int ParseOptions(int argc, char** argv) { for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--list") == 0) { op_ = kList; } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { PrintHelp(argv); return 0; } else if (strcmp(argv[i], "--random-seed") == 0 || strcmp(argv[i], "--random_seed") == 0) { if (++i == argc || !ParseInt(argv[i], &random_seed_)) { return PrintHelp(argv); } } else if (strncmp(argv[i], "--random-seed=", 14) == 0 || strncmp(argv[i], "--random_seed=", 14) == 0) { if (!ParseInt(argv[i] + 14, &random_seed_)) return PrintHelp(argv); } else if (strcmp(argv[i], "--runs") == 0) { if (++i == argc || !ParseInt(argv[i], &runs_)) return PrintHelp(argv); } else if (strncmp(argv[i], "--runs=", 7) == 0) { if (!ParseInt(argv[i] + 7, &runs_)) return PrintHelp(argv); } #define TEST(kName, name) \ else if (strcmp(argv[i], name) == 0) { \ op_ = kTest; \ test_ = kName; \ } TESTS(TEST) #undef TEST else { std::cerr << "Warning: ignored argument: " << argv[i] << "\n"; } } if (op_ == kNoOp) return PrintHelp(argv); // op is mandatory. return 0; } private: void GenerateRandom(RWDigits Z) { if (Z.len() == 0) return; int mode = static_cast(rng_.NextUint64() & 3); if (mode == 0) { // Generate random bits. if (sizeof(digit_t) == 8) { for (int i = 0; i < Z.len(); i++) { Z[i] = static_cast(rng_.NextUint64()); } } else { for (int i = 0; i < Z.len(); i += 2) { uint64_t random = rng_.NextUint64(); Z[i] = static_cast(random); if (i + 1 < Z.len()) Z[i + 1] = static_cast(random >> 32); } } // Special case: we don't want the MSD to be zero. while (Z.msd() == 0) { Z[Z.len() - 1] = static_cast(rng_.NextUint64()); } return; } if (mode == 1) { // Generate a power of 2, with the lone 1-bit somewhere in the MSD. int bit_in_msd = static_cast(rng_.NextUint64() % kDigitBits); Z[Z.len() - 1] = digit_t{1} << bit_in_msd; for (int i = 0; i < Z.len() - 1; i++) Z[i] = 0; return; } // For mode == 2 and mode == 3, generate a random number of 1-bits in the // MSD, aligned to the least-significant end. int bits_in_msd = static_cast(rng_.NextUint64() % kDigitBits); digit_t msd = (digit_t{1} << bits_in_msd) - 1; if (msd == 0) msd = ~digit_t{0}; Z[Z.len() - 1] = msd; if (mode == 2) { // The non-MSD digits are all 1-bits. for (int i = 0; i < Z.len() - 1; i++) Z[i] = ~digit_t{0}; } else { // mode == 3 // Each non-MSD digit is either all ones or all zeros. uint64_t random; int random_bits = 0; for (int i = 0; i < Z.len() - 1; i++) { if (random_bits == 0) { random = rng_.NextUint64(); random_bits = 64; } Z[i] = random & 1 ? ~digit_t{0} : digit_t{0}; random >>= 1; random_bits--; } } } void GenerateRandomString(char* str, int len, int radix) { DCHECK(2 <= radix && radix <= 36); if (len == 0) return; uint64_t random; int available_bits = 0; const int char_bits = BitLength(radix - 1); const uint64_t char_mask = (1u << char_bits) - 1u; for (int i = 0; i < len; i++) { while (true) { if (available_bits < char_bits) { random = rng_.NextUint64(); available_bits = 64; } int next_char = static_cast(random & char_mask); random = random >> char_bits; available_bits -= char_bits; if (next_char >= radix) continue; *str = kConversionChars[next_char]; str++; break; }; } } Operation op_{kNoOp}; Test test_; bool error_{false}; int runs_ = 1; int64_t random_seed_{314159265359}; RNG rng_; std::unique_ptr processor_; }; } // namespace test } // namespace bigint } // namespace v8 int main(int argc, char** argv) { v8::bigint::test::Runner runner; int ret = runner.ParseOptions(argc, argv); if (ret != 0) return ret; runner.Initialize(); return runner.Run(); }