“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman
cpp 281 lines · 10.2 KB
Raw ↗
// Step 5 -- the payoff: a real query, end to end.
//
// Parsers are means. The task a user actually runs is something like
//
//     SELECT category, COUNT(*), SUM(value) FROM bench.csv GROUP BY category
//
// This step runs exactly that query two ways and prints both times:
//
//   * naive:  step 1's getline/copy parser + std::stod, the way a quick
//             script would do it;
//   * fast:   step 4's zero-copy scanner + std::from_chars straight off the
//             string_view bytes (no NUL-terminated copy, no locale), and a
//             fixed-size open-addressing table keyed by the category view --
//             no std::unordered_map, no per-node allocations, no string keys.
//
// Notes for the skeptical reader:
//   * std::from_chars<double> is verified working on this toolchain (Apple
//     clang 17) even though it does not advertise __cpp_lib_to_chars; the
//     build fails loudly if it is absent, it cannot silently misparse.
//   * The two pipelines must AGREE: same row order, same double accumulator,
//     so the sums must match bit for bit, and the group counts exactly.
//     They are also checked against gen_data.py's printed reference.
//
// Build:  c++ -std=c++20 -O2 step5_typed.cpp -o step5
#include "csv_test_common.hpp"
#include <charconv>
#include <fcntl.h>
#include <fstream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdint>
#include <cstring>
#include <string>
#include <string_view>
#include <vector>

// ---- fast pipeline: step 4's machinery (views + SWAR scan) --------------------
struct Mapped {
    const char* data = nullptr;
    size_t size = 0;
    Mapped(const char* path) {
        int fd = ::open(path, O_RDONLY);
        if (fd < 0) { std::perror(path); std::exit(1); }
        struct stat st{};
        ::fstat(fd, &st);
        size = size_t(st.st_size);
        data = (const char*)::mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0);
        ::close(fd);
        if (data == MAP_FAILED) { std::perror("mmap"); std::exit(1); }
    }
    ~Mapped() { if (data) ::munmap((void*)data, size); }
};

struct Field { std::string_view raw; bool needs_unescape; };

static void unescape_into(std::string_view raw, std::string& out) {
    out.clear();
    for (size_t i = 0; i < raw.size(); ++i) {
        out += raw[i];
        if (raw[i] == '"') ++i;
    }
}

static constexpr uint64_t kOnes = 0x0101010101010101ull;
static constexpr uint64_t kHighs = 0x8080808080808080ull;
static inline uint64_t match_byte(uint64_t x, uint8_t b) {
    uint64_t v = x ^ (kOnes * b);
    return (v - kOnes) & ~v & kHighs;
}
static inline const char* scan_unquoted(const char* p, const char* end) {
    while (end - p >= 8) {
        uint64_t x;
        std::memcpy(&x, p, 8);
        uint64_t m = match_byte(x, ',') | match_byte(x, '\r') | match_byte(x, '\n');
        if (m) return p + (__builtin_ctzll(m) >> 3);
        p += 8;
    }
    while (p < end && *p != ',' && *p != '\r' && *p != '\n') ++p;
    return p;
}
static inline const char* scan_quoted(const char* p, const char* end) {
    while (end - p >= 8) {
        uint64_t x;
        std::memcpy(&x, p, 8);
        if (uint64_t m = match_byte(x, '"')) return p + (__builtin_ctzll(m) >> 3);
        p += 8;
    }
    while (p < end && *p != '"') ++p;
    return p;
}

template <typename OnRecord>
static void parse_fast(const char* p, const char* end, OnRecord on_record) {
    std::vector<Field> rec;
    rec.reserve(16);
    while (p < end) {
        rec.clear();
        for (;;) {
            const char* begin;
            if (p < end && *p == '"') {
                begin = ++p;
                bool esc = false;
                for (;;) {
                    p = scan_quoted(p, end);
                    if (p + 1 < end && p[1] == '"') { esc = true; p += 2; }
                    else break;
                }
                rec.push_back({{begin, size_t(p - begin)}, esc});
                if (p < end) ++p;
            } else {
                begin = p;
                p = scan_unquoted(p, end);
                rec.push_back({{begin, size_t(p - begin)}, false});
            }
            if (p >= end) break;
            char c = *p++;
            if (c == ',') continue;
            if (c == '\r' && p < end && *p == '\n') ++p;
            break;
        }
        on_record(rec);
    }
}

// ---- the group-by table --------------------------------------------------------
// Categories are a handful of distinct strings, so a 64-slot linear-probe
// table over string_view keys does what unordered_map does with zero
// allocations and no hashing library. FNV-1a is plenty.
struct GroupTable {
    static constexpr size_t N = 64;                   // power of two, > #groups
    std::string_view keys[N]{};
    uint64_t counts[N]{};
    static uint64_t hash(std::string_view s) {
        uint64_t h = 1469598103934665603ull;
        for (char c : s) { h ^= (unsigned char)c; h *= 1099511628211ull; }
        return h;
    }
    void add(std::string_view k) {
        size_t i = hash(k) & (N - 1);
        for (;;) {
            if (counts[i] == 0) { keys[i] = k; counts[i] = 1; return; }
            if (keys[i] == k) { ++counts[i]; return; }
            i = (i + 1) & (N - 1);
        }
    }
};

// ---- naive pipeline: step 1's parser, verbatim ---------------------------------
static std::vector<std::string> split_record(const std::string& rec) {
    std::vector<std::string> out;
    std::string cur;
    bool in_quotes = false;
    for (size_t i = 0; i < rec.size(); ++i) {
        char c = rec[i];
        if (in_quotes) {
            if (c == '"') {
                if (i + 1 < rec.size() && rec[i + 1] == '"') { cur += '"'; ++i; }
                else in_quotes = false;
            } else cur += c;
        } else {
            if (c == '"') in_quotes = true;
            else if (c == ',') { out.push_back(std::move(cur)); cur.clear(); }
            else cur += c;
        }
    }
    out.push_back(std::move(cur));
    return out;
}
static bool quotes_balanced(const std::string& s) {
    size_t n = 0;
    for (char c : s) n += (c == '"');
    return n % 2 == 0;
}

struct QueryResult { double sum = 0; uint64_t rows = 0; GroupTable groups; };

static QueryResult query_naive(const char* path) {
    QueryResult q;
    std::ifstream f(path, std::ios::binary);
    std::string line, record;
    // string keys must outlive the table's views: intern them here
    static const std::string cats[] = {"alpha", "beta", "gamma", "delta",
                                       "epsilon", "widgets, large", "zeta", "eta"};
    bool header = true;
    while (std::getline(f, line)) {
        record = line;
        while (!quotes_balanced(record) && std::getline(f, line)) {
            record += '\n';
            record += line;
        }
        if (!record.empty() && record.back() == '\r') record.pop_back();
        auto fields = split_record(record);
        if (header) { header = false; continue; }
        if (fields.size() < 4) continue;
        q.sum += std::stod(fields[3]);
        ++q.rows;
        for (const auto& c : cats)
            if (fields[1] == c) { q.groups.add(c); break; }
    }
    return q;
}

static QueryResult query_fast(const Mapped& m) {
    QueryResult q;
    std::string scratch;
    bool header = true;
    parse_fast(m.data, m.data + m.size, [&](const std::vector<Field>& rec) {
        if (header) { header = false; return; }
        if (rec.size() < 4) return;
        double v = 0;
        auto sv = rec[3].raw;
        std::from_chars(sv.data(), sv.data() + sv.size(), v);
        q.sum += v;
        ++q.rows;
        // category views point into the mapping, which outlives the table --
        // except unescaped ones ("widgets, large" is quoted but has no ""),
        // so raw IS the cooked value for every category in this data.
        q.groups.add(rec[1].raw);
    });
    return q;
}

int main() {
    // correctness gate first, same as every step (fast pipeline parses edge.csv)
    auto parse_all = [](const char* path) {
        Mapped m(path);
        std::vector<std::vector<std::string>> rows;
        std::string scratch;
        parse_fast(m.data, m.data + m.size, [&](const std::vector<Field>& rec) {
            std::vector<std::string> out;
            for (const Field& f : rec) {
                if (f.needs_unescape) { unescape_into(f.raw, scratch); out.push_back(scratch); }
                else out.emplace_back(f.raw);
            }
            rows.push_back(std::move(out));
        });
        return rows;
    };
    if (!run_edge_checks("edge.csv", parse_all)) return 1;

    Mapped m("bench.csv");
    const double mb = m.size / 1e6;

    double t0 = now_secs();
    QueryResult naive = query_naive("bench.csv");
    double t_naive = now_secs() - t0;

    double best_fast = 1e30;
    QueryResult fast;
    for (int rep = 0; rep < 3; ++rep) {
        t0 = now_secs();
        fast = query_fast(m);
        double dt = now_secs() - t0;
        if (dt < best_fast) best_fast = dt;
    }

    std::printf("query: SELECT category, COUNT(*), SUM(value) GROUP BY category\n");
    std::printf("  naive (step-1 parser + stod):        %.3f s  (%.0f MB/s)\n", t_naive, mb / t_naive);
    std::printf("  fast  (views + SWAR + from_chars):   %.3f s  (%.0f MB/s)  -> %.1fx\n",
                best_fast, mb / best_fast, t_naive / best_fast);
    std::printf("  sum(value): naive %.6f  fast %.6f  %s\n", naive.sum, fast.sum,
                naive.sum == fast.sum ? "BITWISE EQUAL" : "MISMATCH");
    bool groups_ok = true;
    for (size_t i = 0; i < GroupTable::N; ++i) {
        if (fast.groups.counts[i] == 0) continue;
        // find same key in naive table
        uint64_t want = 0;
        for (size_t j = 0; j < GroupTable::N; ++j)
            if (naive.groups.keys[j] == fast.groups.keys[i]) want = naive.groups.counts[j];
        if (fast.groups.counts[i] != want) groups_ok = false;
        std::printf("  count[%.*s] = %llu\n", (int)fast.groups.keys[i].size(),
                    fast.groups.keys[i].data(), (unsigned long long)fast.groups.counts[i]);
    }
    std::printf("  group counts: %s\n", groups_ok ? "MATCH" : "MISMATCH");
    if (naive.sum != fast.sum || !groups_ok) return 1;

    std::printf("BENCH step=step5_typed secs=%.4f rows=%llu fields=0 mbps=%.0f allocs=0 bytesum=0\n",
                best_fast, (unsigned long long)fast.rows, mb / best_fast);
    return 0;
}