Chapter 53Project Top K Word Frequency Analyzer

Project

概述

调试章节介绍了用于解释程序为何表现不当的工具。52 本项目采用类似的原则来构建确定性文本分析工具:输入日志摘录,收集最频繁的标记,并发出每个阶段的时序数据。我们将结合std.mem中的标记化辅助函数、std中的哈希集合、heap排序器和Timer API来生成可重现的排名和可测量的成本。mem.zighash_map.zigsort.zigtime.zig

学习目标

  • 构建端到端I/O管道,使用std.StringHashMapstd.ArrayList读取语料库、规范化文本并累积计数。array_list.zig
  • 使用显式比较器确定性排名频率,而不依赖于哈希映射迭代顺序来解析平局。
  • 使用std.time.Timer捕获每个阶段的时序,以验证回归并传达性能期望。

设计管道

我们的分析器接受可选的文件路径和可选的k参数(top k标记);两者分别默认为捆绑的语料库和5。为简单起见,我们将整个文件读入内存,但规范化和计数循环被编写为线性操作,因此可以稍后适应流式传输块。GeneralPurposeAllocator支持所有动态结构,arena友好的工作流程(仅在首次看到时复制字符串)使分配与词汇大小成比例。heap.zigprocess.zig

标记化使用std.mem.tokenizeAny进行,配置了保守的分隔符集,可修剪空白、标点和标记字符。每个标记在尝试插入映射之前在可重用的std.ArrayList(u8)中转换为小写;如果标记已存在,则仅增加计数,保持临时分配有界。

计数和排名

完整的实用工具演示了StringHashMap、ArrayList、排序和时序的并行操作。

Zig
const std = @import("std");

const Separators = " \t\r\n,.;:!?\"'()[]{}<>-/\\|`~*_";

const Entry = struct {
    word: []const u8,
    count: usize,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer if (gpa.deinit() == .leak) @panic("memory leak");
    const allocator = gpa.allocator();

    var stdout_buffer: [256]u8 = undefined;
    var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
    const out = &stdout_writer.interface;

    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);

    const path = if (args.len >= 2) args[1] else "chapters-data/code/53__project-top-k-word-frequency-analyzer/sample_corpus.txt";
    const top_k: usize = blk: {
        if (args.len >= 3) {
            const value = std.fmt.parseInt(usize, args[2], 10) catch {
                try out.print("invalid top-k value: {s}\n", .{args[2]});
                return error.InvalidArgument;
            };
            break :blk if (value == 0) 1 else value;
        }
        break :blk 5;
    };

    var timer = try std.time.Timer.start();

    const corpus = try std.fs.cwd().readFileAlloc(allocator, path, 1024 * 1024 * 4);
    defer allocator.free(corpus);
    const read_ns = timer.lap();

    var scratch = try std.ArrayList(u8).initCapacity(allocator, 32);
    defer scratch.deinit(allocator);

    var frequencies = std.StringHashMap(usize).init(allocator);
    defer frequencies.deinit();

    var total_tokens: usize = 0;

    var it = std.mem.tokenizeAny(u8, corpus, Separators);
    while (it.next()) |raw| {
        scratch.clearRetainingCapacity();
        try scratch.appendSlice(allocator, raw);
        const slice = scratch.items;
        for (slice) |*byte| {
            byte.* = std.ascii.toLower(byte.*);
        }
        if (slice.len == 0) continue;

        const gop = try frequencies.getOrPut(slice);
        if (gop.found_existing) {
            gop.value_ptr.* += 1;
        } else {
            const owned = try allocator.dupe(u8, slice);
            gop.key_ptr.* = owned;
            gop.value_ptr.* = 1;
        }
        total_tokens += 1;
    }
    const tokenize_ns = timer.lap();

    var entries = try std.ArrayList(Entry).initCapacity(allocator, frequencies.count());
    defer {
        for (entries.items) |entry| allocator.free(entry.word);
        entries.deinit(allocator);
    }

    var map_it = frequencies.iterator();
    while (map_it.next()) |kv| {
        try entries.append(allocator, .{ .word = kv.key_ptr.*, .count = kv.value_ptr.* });
    }

    const entry_slice = entries.items;
    std.sort.heap(Entry, entry_slice, {}, struct {
        fn lessThan(_: void, a: Entry, b: Entry) bool {
            if (a.count == b.count) return std.mem.lessThan(u8, a.word, b.word);
            return a.count > b.count;
        }
    }.lessThan);
    const sort_ns = timer.lap();

    const unique_words = entries.items.len;
    const limit = if (unique_words < top_k) unique_words else top_k;

    try out.print("source -> {s}\n", .{path});
    try out.print("tokens -> {d}, unique -> {d}\n", .{ total_tokens, unique_words });
    try out.print("top {d} words:\n", .{limit});
    var index: usize = 0;
    while (index < limit) : (index += 1) {
        const entry = entry_slice[index];
        try out.print("  {d:>2}. {s} -> {d}\n", .{ index + 1, entry.word, entry.count });
    }

    try out.print("timings (ns): read={d}, tokenize={d}, sort={d}\n", .{ read_ns, tokenize_ns, sort_ns });
    try out.flush();
}
运行
Shell
$ zig run chapters-data/code/53__project-top-k-word-frequency-analyzer/topk_word_frequency.zig
输出
Shell
source -> chapters-data/code/53__project-top-k-word-frequency-analyzer/sample_corpus.txt
tokens -> 102, unique -> 86
top 5 words:
   1. the -> 6
   2. a -> 3
   3. and -> 3
   4. are -> 2
   5. latency -> 2
timings (ns): read=284745, tokenize=3390822, sort=236725

std.StringHashMap存储规范的小写拼写,单独的std.ArrayList收集最终的(word, count)对进行排序。我们选择std.sort.heap,因为它是确定性的,没有分配器依赖,并且在小型数据集上表现良好;比较器主要按降序计数排序,其次按词法排序以保持平局稳定。当跨运行或机器重新运行分析时,这一点很重要——现场团队可以毫无意外地差异结果CSV。

时序和可重现性

单个Timer实例测量三个阶段:文件摄取、标记化和排序。我们在每个阶段后调用lap()来重置零点,同时记录经过的纳秒数,使您能够轻松识别哪个步骤占主导地位。因为分析器规范化大小写并使用确定性排序,给定语料库的输出在不同运行中保持相同,允许将时序差异归因于硬件或工具链变化,而不是非确定性排序。

对于回归,使用更大的k或不同的语料库重新运行:

Shell
$ zig run chapters-data/code/53__project-top-k-word-frequency-analyzer/topk_word_frequency.zig -- chapters-data/code/53__project-top-k-word-frequency-analyzer/sample_corpus.txt 10

可选参数让您保持二进制文件可脚本化——将其放入CI中,比较输出工件,并在时序预算变化超过阈值时发出警报。当集成到更大的系统中时,映射构建循环可以交换为从stdin或TCP套接字流式传输,同时保持相同的确定性排名规则。File.zig

注意事项和警告

  • StringHashMap不会自动释放存储的键;此示例在删除映射之前显式释放它们,以保持通用分配器泄漏检查器满意。
  • 标记化器专注于ASCII。对于完整的Unicode分割,将管道与std.unicode.ScalarIterator配对或集成ICU绑定。45
  • 将整个语料库读入内存简化了教程,但可能不适合多千兆字节的日志。在扩展时,将readFileAlloc交换为分块的readAll循环或内存映射文件。28

练习

  • 通过序列化排序的切片以JSON形式发出报告,然后与文本版本比较差异友好性。32json.zig
  • 用两阶段管道替换单线程分析器:跨线程分片标记,然后在排序前合并哈希映射。使用Timer测量收益并总结扩展性。29
  • 添加一个--stopwords选项,加载以换行符分隔的忽略列表,在计数前删除这些标记,并报告过滤掉了多少候选。36

警告、替代方案和边界情况

  • 对于流式环境,考虑使用std.PriorityQueue以增量方式维护top k,而不是在排序前记录整个直方图。priority_queue.zig
  • 如果性能要求超出堆排序,尝试使用std.sort.pdq或基于桶的方法,同时保持确定性比较器契约完整。
  • 为了支持多语言文本,叠加规范化(NFC/NFKC)并使用Unicode感知的大小写辅助函数;比较器可能需要特定于语言环境的排序规则以保持结果直观。45

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.