diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..45bf610 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,171 @@ +name: build + +on: + workflow_call: + inputs: + coverage: + type: boolean + default: false + scan: + type: boolean + default: false + doc: + type: boolean + default: false + + workflow_dispatch: + inputs: + coverage: + type: boolean + default: false + scan: + type: boolean + default: false + doc: + type: boolean + default: false + +jobs: + build: + runs-on: ubuntu-latest + + container: + image: joinframework/join-ci:latest + + permissions: + contents: read + security-events: write + + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Build Environment + if: ${{ inputs.scan || inputs.coverage }} + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Initialize codeql + if: ${{ inputs.scan }} + uses: github/codeql-action/init@v3 + + - name: Configure + if: ${{ inputs.scan || inputs.coverage }} + run: cmake -B build -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug -DDCONV_ENABLE_COVERAGE=ON -DDCONV_ENABLE_TESTS=ON + + - name: Build + if: ${{ inputs.scan || inputs.coverage }} + run: cmake --build build --config Debug + + - name: Run codeql scan + if: ${{ inputs.scan }} + uses: github/codeql-action/analyze@v3 + + - name: Run tests + if: ${{ inputs.coverage }} + run: ctest --test-dir build --output-on-failure -C Debug + + - name: Generate coverage report + if: ${{ inputs.coverage }} + run: | + lcov --directory build --capture --exclude "*/usr/include/*" --exclude '*/tests/*' --output-file lcov.info + genhtml lcov.info --prefix $GITHUB_WORKSPACE --output-directory coverage + + - name: Upload coverage data to GitHub + if: ${{ inputs.coverage }} + uses: actions/upload-artifact@v4 + with: + name: coverage-data + path: lcov.info + + - name: Upload coverage report to GitHub + if: ${{ inputs.coverage }} + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + + - name: Run doxygen + if: ${{ inputs.doc }} + uses: mattnotmitt/doxygen-action@v1 + with: + doxyfile-path: ./doxyfile + working-directory: . + + - name: Upload documentation to GitHub + if: ${{ inputs.doc }} + uses: actions/upload-pages-artifact@v3 + with: + path: ./doc/html + + deploy-coverage: + if: ${{ inputs.coverage }} + + runs-on: ubuntu-latest + + container: + image: joinframework/join-ci:latest + + permissions: + contents: read + + needs: build + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Download coverage data from GitHub + uses: actions/download-artifact@v4 + with: + name: coverage-data + path: ./ + + - name: Deploy coverage report to coveralls + uses: coverallsapp/github-action@v2 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + files: lcov.info + + - name: Deploy coverage report to codecov + uses: codecov/codecov-action@v5 + with: + token: ${{secrets.CODECOV_TOKEN}} + flags: unittests + files: lcov.info + fail_ci_if_error: true + + - name: Deploy coverage report to codacy + uses: codacy/codacy-coverage-reporter-action@v1 + with: + project-token: ${{secrets.CODACY_PROJECT_TOKEN}} + coverage-reports: lcov.info + + deploy-pages: + if: ${{ inputs.doc }} + + runs-on: ubuntu-latest + + container: + image: joinframework/join-ci:latest + + permissions: + contents: read + pages: write + id-token: write + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + needs: build + + steps: + - name: Deploy documentation to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 009b8fb..0000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: coverage - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - scan: - runs-on: ubuntu-22.04 - - container: - image: joinframework/join-ci:latest - options: --privileged --sysctl net.ipv6.conf.all.disable_ipv6=0 - - steps: - - name: Checkout - uses: actions/checkout@v3.5.0 - - - name: Configure - run: cmake -B build -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug -DDCONV_ENABLE_COVERAGE=ON -DDCONV_ENABLE_TESTS=ON - - - name: Build - run: cmake --build build --config Debug - - - name: Run tests - run: ctest --test-dir build --output-on-failure -C Debug - - - name: Generate coverage report - run: | - lcov --directory build --capture --exclude "*/usr/include/*" --exclude '*/tests/*' --output-file lcov.info - genhtml lcov.info --prefix $GITHUB_WORKSPACE --output-directory coverage - - - name: Upload coverage data to GitHub - if: ${{ inputs.coverage }} - uses: actions/upload-artifact@v4 - with: - name: coverage-data - path: lcov.info - - - name: Upload coverage report to coveralls - uses: coverallsapp/github-action@v2 - with: - github-token: ${{secrets.GITHUB_TOKEN}} - file: lcov.info - - - name: Upload coverage report to codecov - uses: codecov/codecov-action@v3.1.1 - with: - fail_ci_if_error: true - token: ${{secrets.CODECOV_TOKEN}} - flags: unittests - file: lcov.info - - - name: Upload coverage report to codacy - uses: codacy/codacy-coverage-reporter-action@v1.3.0 - with: - project-token: ${{secrets.CODACY_PROJECT_TOKEN}} - coverage-reports: lcov.info diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml deleted file mode 100644 index 5faa904..0000000 --- a/.github/workflows/doxygen.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: doxygen - -on: - push: - branches: [ main ] - -jobs: - documentation: - runs-on: ubuntu-22.04 - - steps: - - name: Checkout - uses: actions/checkout@v3.0.2 - - - name: Run doxygen - uses: mattnotmitt/doxygen-action@v1.9.5 - with: - doxyfile-path: ./doxyfile - working-directory: . - - - name: Upload documentation to github pages - uses: peaceiris/actions-gh-pages@v3.9.2 - with: - github_token: ${{secrets.GITHUB_TOKEN}} - publish_dir: ./doc/html/ - enable_jekyll: false - allow_empty_commit: false - force_orphan: true - publish_branch: gh-pages diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..17d41bf --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,22 @@ +name: pages + +on: + push: + branches: [ main ] + + workflow_dispatch: + +jobs: + generate: + uses: ./.github/workflows/build.yml + + secrets: inherit + + with: + doc: true + + permissions: + contents: read + security-events: write + pages: write + id-token: write diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index bc1266b..eb87707 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -3,47 +3,26 @@ name: security on: push: branches: [ main ] + pull_request: branches: [ main ] + schedule: - - cron: "51 9 * * 5" + - cron: "0 11 * * 0" + + workflow_dispatch: jobs: scan: - runs-on: ubuntu-22.04 + uses: ./.github/workflows/build.yml + + secrets: inherit + + with: + scan: true permissions: - actions: read contents: read security-events: write - - steps: - - name: Checkout - uses: actions/checkout@v3.5.0 - - - name: Perform codacy analysis - uses: codacy/codacy-analysis-cli-action@v4.3.0 - with: - project-token: ${{secrets.CODACY_PROJECT_TOKEN}} - verbose: true - output: results.sarif - format: sarif - gh-code-scanning-compat: true - max-allowed-issues: 2147483647 - - - name: Upload codacy scan report to github - uses: github/codeql-action/upload-sarif@v2 - with: - sarif_file: results.sarif - - - name: Initialize codeql - uses: github/codeql-action/init@v2 - - - name: Configure - run: cmake -B _lgtm_build_dir -G "Unix Makefiles" -DDCONV_ENABLE_TESTS=ON - - - name: Build - run: cmake --build _lgtm_build_dir --config Debug - - - name: Perform codeql analysis - uses: github/codeql-action/analyze@v2 + pages: write + id-token: write diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0c59e32 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,25 @@ +name: test + +on: + push: + branches: [ main ] + + pull_request: + branches: [ main ] + + workflow_dispatch: + +jobs: + coverage: + uses: ./.github/workflows/build.yml + + secrets: inherit + + with: + coverage: true + + permissions: + contents: read + security-events: write + pages: write + id-token: write diff --git a/README.md b/README.md index b8edeb2..565602f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Double to string conversion is done using the **Grisu2** algorithm, described by **Florian Loitsch** in its publication [Printing Floating-Point Numbers Quickly and Accurately with Integers](https://florian.loitsch.com/publications). -String to double conversion is done using a fast and simple (but not accurate!) approach and fallback to **strtod** if conversion can't be done the simplified way. +String to double conversion uses the **Eisel-Lemire** algorithm for fast parsing with a fallback to **strtod** for cases requiring higher precision. The code is far from being perfect so any help to improve speed, accuratie, code quality etc... is welcome. diff --git a/dconv/include/dconv/atod.hpp b/dconv/include/dconv/atod.hpp index c8d8d95..b6a4bbc 100644 --- a/dconv/include/dconv/atod.hpp +++ b/dconv/include/dconv/atod.hpp @@ -30,8 +30,10 @@ // C++. #include +#include // C. +#include #include #include #include @@ -43,52 +45,203 @@ namespace dconv { namespace details { + struct LocaleDelete + { + constexpr LocaleDelete () noexcept = default; + + void operator () (locale_t loc) + { + freelocale (loc); + } + }; + + using LocalePtr = std::unique_ptr , LocaleDelete>; + inline const char * strtodSlow (const char * beg, double& value) { + static LocalePtr locale (newlocale (LC_ALL_MASK, "C", nullptr)); char* end = nullptr; - static locale_t locale = newlocale (LC_ALL_MASK, "C", nullptr); - value = strtod_l (beg, &end, locale); + value = strtod_l (beg, &end, locale.get ()); return end; } - inline bool strtodFast (bool negative, uint64_t mantissa, int64_t exponent, double& value) + struct CachedPower { - static const double pow10[] = { - 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, - 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, - 1e22 - }; + uint64_t mul; + int16_t pow2; + }; + + constexpr CachedPower pow10[80] = { + {0xff77b1fcbebcdc4f, -1196}, {0x8dd01fad907ffc3b, -1131}, + {0xd3515c2831559a83, -1067}, {0x9d71ac8fada6c9b5, -1004}, + {0xea9c227723ee8bcb, -940}, {0xaecc49914078536d, -877}, + {0x823c12795db6ce57, -814}, {0xc21094364dfb5636, -751}, + {0x9096ea6f3848984f, -688}, {0xe096f02c5c4b3e04, -625}, + {0xab70fe17c79ac6ca, -562}, {0xfee50b7025c36a08, -499}, + {0xbd8430bd08277231, -436}, {0x8bab8eefb6409c1a, -373}, + {0xd89d64d57a607744, -310}, {0x9f4f2726179a2245, -248}, + {0xed63a231d4c4fb27, -185}, {0xb0de65388cc8ada8, -122}, + {0x83c7088e1aab65db, -60}, {0xc45d1df942711d9a, 3}, + {0x924d692ca61be758, 66}, {0xe12e13424bb40e13, 128}, + {0xac5d37d5b79b6239, 191}, {0xfd87b5f28300ca0d, 253}, + {0xbce5086492111aea, 316}, {0x8cbccc096f5088cb, 379}, + {0xd1b71758e219652b, 441}, {0x9c40000000000000, 504}, + {0xe8d4a51000000000, 566}, {0xad78ebc5ac620000, 629}, + {0x813f3978f8940984, 692}, {0xc097ce7bc90715b3, 754}, + {0x8f7e32ce7bea5c6f, 817}, {0xd5d238a4abe98068, 879}, + {0x9becce62836ac577, 942}, {0xe858ad248f5c22c9, 1004}, + {0xaf298d050e4395d6, 1067}, {0x82818f1281ed449f, 1130}, + {0xc2781f49ffcfa6d5, 1192}, {0x8e679c2f5e44ff8f, 1255}, + {0xd433179d9c8cb841, 1317}, {0x9abe14cd44753b52, 1380}, + {0xe596b7b0c643c719, 1442}, {0xaf87023b9bf0ee6a, 1505}, + {0x8000000000000000, 1568}, {0xc000000000000000, 1630}, + {0x8e1bc9bf04000000, 1693}, {0xd3c21bcecceda100, 1755}, + {0x9a130b963a6c115c, 1818}, {0xe592d025b79fc475, 1880}, + {0xae75f2a2b9cd4f7a, 1943}, {0x823c12795db6ce57, 2006}, + {0xbff8f10e7a8921a4, 2068}, {0x8dd01fad907ffc3b, 2131}, + {0xd5605fcdcf32e1d6, 2193}, {0x9b10a4e5e9913128, 2256}, + {0xe7109bfba19c0c9d, 2318}, {0xac2820d9623bf429, 2381}, + {0xfea126b7d78186bc, 2443}, {0xba756174393d88df, 2506}, + {0x8a08f0f8bf0f156b, 2569}, {0xd226fc195c6a2f8c, 2631}, + {0x97c560ba6b0919a5, 2694}, {0xe2250f2b6b2c1e8e, 2756}, + {0xa8d9d1535ce3b396, 2819}, {0xfb9b7cd9a4a7443c, 2881}, + {0xb8157268fdae9e4c, 2944}, {0x8858ce5d8a3a7cf0, 3007}, + {0xcdb02555653131b6, 3069}, {0x952e5d8e1e4e8a81, 3132}, + {0xdd5e8b9afacbe17d, 3194}, {0xa1258379a94d028d, 3257}, + {0xf08045f99aded67b, 3319}, {0xb3f4e093db73a093, 3382}, + {0x87625f056c7c4a8b, 3445}, {0xc90715b34b9f1000, 3507}, + {0x92efd1b8d0cf37be, 3570}, {0xd77485cb25823ac7, 3632}, + {0x9eb9faad07929684, 3695}, {0xe9e7cea3f88b0c64, 3757}, + }; + + static constexpr uint64_t pow10Small[8] = { + 1ULL, 10ULL, 100ULL, 1000ULL, 10000ULL, 100000ULL, 1000000ULL, 10000000ULL + }; + + inline void umul128 (uint64_t a, uint64_t b, uint64_t& hi, uint64_t& lo) noexcept + { + #if defined(__SIZEOF_INT128__) + __uint128_t product = static_cast <__uint128_t> (a) * b; + hi = static_cast (product >> 64); + lo = static_cast (product); + #else + uint64_t a_lo = static_cast (a); + uint64_t a_hi = a >> 32; + uint64_t b_lo = static_cast (b); + uint64_t b_hi = b >> 32; + + uint64_t p0 = a_lo * b_lo; + uint64_t p1 = a_lo * b_hi; + uint64_t p2 = a_hi * b_lo; + uint64_t p3 = a_hi * b_hi; + + uint64_t middle = p1 + (p0 >> 32) + static_cast (p2); + + lo = (middle << 32) | static_cast (p0); + hi = p3 + (middle >> 32) + (p2 >> 32); + #endif + } - if (mantissa == 0) + inline bool strtodFast (bool negative, uint64_t mantissa, int64_t exponent, double& value) + { + if (unlikely (mantissa == 0)) { value = negative ? -0.0 : 0.0; return true; } - if (likely ((exponent >= -22) && (exponent <= 22) && (mantissa <= 9007199254740991))) + if (unlikely (exponent < -325 || exponent > 308)) { - value = static_cast (mantissa); - if (exponent < 0) - { - value /= pow10[-exponent]; - } - else + return false; + } + + int32_t q = static_cast (exponent) + 325; + int32_t k = q / 8; + int32_t r = q & 7; + + if (unlikely (k >= 80)) + { + return false; + } + + if (r > 0) + { + mantissa *= pow10Small[r]; + } + + int clz = __builtin_clzll (mantissa); + if (unlikely (clz < 1)) + { + return false; + } + + mantissa <<= clz; + + const CachedPower& cp = pow10[k]; + uint64_t hi, lo; + umul128 (mantissa, cp.mul, hi, lo); + + int32_t binExp = cp.pow2 + 64 - clz - 1; + + if ((hi & 0x8000000000000000ULL) == 0) + { + hi = (hi << 1) | (lo >> 63); + lo = lo << 1; + binExp--; + } + + int32_t ieeeExp = binExp + 1023; + + if (ieeeExp < 1 || ieeeExp > 2046) + { + return false; + } + + uint64_t mantissaBits = hi >> 11; + + // uint64_t lowerBits = (hi & 0x7FFULL) | (lo != 0 ? 1ULL : 0ULL); + // if (lowerBits == 0x400ULL || lowerBits == 0x3FFULL) + // { + // return false; + // } + + uint64_t roundBit = (hi >> 10) & 1; + uint64_t stickyBits = ((hi & 0x3FFULL) | lo) != 0 ? 1ULL : 0ULL; + + if (roundBit && (stickyBits || (mantissaBits & 1))) + { + mantissaBits++; + + if (mantissaBits > 0x1FFFFFFFFFFFFFULL) { - value *= pow10[exponent]; + mantissaBits >>= 1; + ieeeExp++; + + if (ieeeExp > 2046) + { + return false; + } } - value = negative ? -value : value; - return true; } - return false; + mantissaBits &= 0xFFFFFFFFFFFFFULL; + uint64_t bits = (static_cast (ieeeExp) << 52) | mantissaBits; + + if (negative) + { + bits |= 0x8000000000000000ULL; + } + + std::memcpy (&value, &bits, sizeof(double)); + return true; } - inline bool isDigit (char c) + inline bool isDigit (char c) noexcept { - return (c >= '0') && (c <= '9'); + return static_cast (c - '0') <= 9u; } - inline bool isSign (char c) + inline bool isSign (char c) noexcept { return (c == '+') || (c == '-'); } @@ -97,7 +250,8 @@ namespace dconv { auto beg = view.data (); - uint64_t mantissa = 0, digits = 0; + uint64_t mantissa = 0; + uint64_t digits = 0; bool neg = view.getIf ('-'); if (view.getIf ('0')) @@ -107,36 +261,39 @@ namespace dconv return nullptr; } } - else if (isDigit (view.peek ())) + else if (likely (isDigit (view.peek ()))) { mantissa = view.get () - '0'; ++digits; while (isDigit (view.peek ())) { - if (unlikely (mantissa > (std::numeric_limits ::max () / 10))) + uint64_t next = (10 * mantissa) + (view.get () - '0'); + if (unlikely (next < mantissa)) { return strtodSlow (beg, value); } - mantissa = (10 * mantissa) + (view.get () - '0'); + mantissa = next; ++digits; } } else if (view.getIfNoCase ('i') && view.getIfNoCase ('n') && view.getIfNoCase ('f')) { - if (view.getIfNoCase ('i') && !(view.getIfNoCase ('n') && view.getIfNoCase ('i') && view.getIfNoCase ('t') && view.getIfNoCase ('y'))) + if (view.getIfNoCase ('i')) { - return nullptr; + if (!(view.getIfNoCase ('n') && view.getIfNoCase ('i') && + view.getIfNoCase ('t') && view.getIfNoCase ('y'))) + { + return nullptr; + } } value = neg ? -std::numeric_limits ::infinity () : std::numeric_limits ::infinity (); - return view.data (); } else if (view.getIfNoCase ('n') && view.getIfNoCase ('a') && view.getIfNoCase ('n')) { value = neg ? -std::numeric_limits ::quiet_NaN () : std::numeric_limits ::quiet_NaN (); - return view.data (); } else @@ -159,11 +316,12 @@ namespace dconv while (isDigit (view.peek ())) { - if (unlikely (mantissa > (std::numeric_limits ::max () / 10))) + uint64_t next = (10 * mantissa) + (view.get () - '0'); + if (unlikely (next < mantissa)) { return strtodSlow (beg, value); } - mantissa = (10 * mantissa) + (view.get () - '0'); + mantissa = next; if (mantissa || digits) ++digits; --exponent; } @@ -171,11 +329,11 @@ namespace dconv if (view.getIf ('e') || view.getIf ('E')) { - bool negexp = false; + bool negExp = false; if (isSign (view.peek ())) { - negexp = (view.get () == '-'); + negExp = (view.get () == '-'); } if (unlikely (!isDigit (view.peek ()))) @@ -187,7 +345,7 @@ namespace dconv while (isDigit (view.peek ())) { - if (likely (exp < 0x100000000)) + if (likely (exp < 1000)) { exp = (10 * exp) + (view.get () - '0'); } @@ -197,10 +355,10 @@ namespace dconv } } - exponent += (negexp ? -exp : exp); + exponent += (negExp ? -exp : exp); } - if (likely ((exponent >= -325) && (exponent <= 308) && (digits <= 19))) + if (likely (digits <= 19)) { if (strtodFast (neg, mantissa, exponent, value)) { diff --git a/dconv/include/dconv/dtoa.hpp b/dconv/include/dconv/dtoa.hpp index 94bfe60..2f9a4df 100644 --- a/dconv/include/dconv/dtoa.hpp +++ b/dconv/include/dconv/dtoa.hpp @@ -562,8 +562,8 @@ namespace dconv minus *= c_mk; plus *= c_mk; - ++minus._mantissa; - --plus._mantissa; + --minus._mantissa; + ++plus._mantissa; k = -mk;