Guest User

Untitled

a guest
Sep 9th, 2026
15
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 6.34 KB | None | 0 0
  1. Reproducing the 'alphasort' benchmark: 167,863,639 rows / 6,061,752,179 tokens
  2. # Machine: Windows 11, 32 GB RAM, 18 threads, SSD. Git Bash for GNU sort.
  3.  
  4. ## 1. Generate the corpus (~32 GB, ~12 min). python gen_corpus.py corpus.txt
  5.  
  6. # Generates a tokenized corpus matching the claim: 167,863,639 rows, 6,061,752,179 tokens (~30 GB).
  7. # Zipf-distributed vocabulary of 200k lowercase words, space-separated, one row per line.
  8. import sys, time
  9. import numpy as np
  10.  
  11. ROWS, TOKENS, V = 167_863_639, 6_061_752_179, 200_000
  12. out = sys.argv[1] if len(sys.argv) > 1 else "corpus.txt"
  13. rng = np.random.default_rng(42)
  14.  
  15. # vocab: unique random lowercase words, frequent (low-rank) words shorter, like real text
  16. lengths = rng.integers(2, 7, V); lengths[:20000] = rng.integers(1, 4, 20000)
  17. letters = rng.integers(97, 123, (V, 8), dtype=np.uint8)
  18. mask = np.arange(8) < lengths[:, None]
  19. vocab = np.unique(np.where(mask, letters, 0).view('S8').ravel())
  20. rng.shuffle(vocab); vocab = vocab[:V]; V = len(vocab)
  21. tok_sp = np.char.add(vocab, b' ').astype('S9') # token + space
  22. tok_nl = np.char.add(vocab, b'\n').astype('S9') # token + newline (row end)
  23. p = 1.0 / np.arange(1, V + 1) ** 1.05; p /= p.sum()
  24.  
  25. base, extra = divmod(TOKENS, ROWS) # base tokens per row; first `extra` rows get one more
  26. CH = 500_000 # rows per chunk
  27. rows_done = toks_done = 0
  28. t0 = time.perf_counter()
  29. with open(out, 'wb') as f:
  30. while rows_done < ROWS:
  31. r = min(CH, ROWS - rows_done)
  32. lens = np.full(r, base, dtype=np.int64)
  33. lens[: max(0, min(r, extra - rows_done))] += 1
  34. n = int(lens.sum())
  35. idx = rng.choice(V, size=n, p=p)
  36. last = np.zeros(n, dtype=bool); last[np.cumsum(lens) - 1] = True
  37. chunk = np.where(last, tok_nl[idx], tok_sp[idx]).tobytes().replace(b'\x00', b'')
  38. f.write(chunk)
  39. rows_done += r; toks_done += n
  40. if rows_done % 5_000_000 == 0 or rows_done == ROWS:
  41. print(f"{rows_done:,} rows {toks_done:,} tokens {time.perf_counter()-t0:.0f}s", flush=True)
  42. assert rows_done == ROWS and toks_done == TOKENS, (rows_done, toks_done)
  43. print("done")
  44.  
  45. ## 2. GNU sort baseline (45 min on this machine)
  46.  
  47. #!/bin/bash
  48. # Sort every token in corpus.txt with GNU sort, timed. Run from Git Bash.
  49. cd "$(dirname "$0")"
  50. export LC_ALL=C # byte order, several x faster than locale collation
  51. mkdir -p tmp
  52. echo "start: $(date)"
  53. SECONDS=0
  54. tr ' ' '\n' < corpus.txt | sort -S 20G --parallel=16 -T tmp > sorted_tokens.txt
  55. echo "sort took ${SECONDS}s ($(($SECONDS/60)) min)"
  56. echo "lines: $(wc -l < sorted_tokens.txt) (expect 6,061,752,179)"
  57. echo "verify: $(sort -c sorted_tokens.txt && echo SORTED)"
  58.  
  59. ## 3. Counting sort, Python (20 min). python count_sort.py corpus.txt out.txt
  60.  
  61. # Counting sort: one pass to count tokens, sort the small vocabulary, write each token count times.
  62. # Output is byte-identical to `tr ' ' '\n' < corpus.txt | LC_ALL=C sort`.
  63. import sys, time
  64. from collections import Counter
  65.  
  66. src, dst = sys.argv[1], sys.argv[2]
  67. t0 = time.perf_counter()
  68. counts = Counter()
  69. with open(src, 'rb') as f:
  70. tail = b''
  71. while chunk := f.read(1 << 27):
  72. chunk = tail + chunk
  73. chunk, _, tail = chunk.rpartition(b'\n')
  74. counts.update(chunk.split())
  75. counts.update(tail.split())
  76. n = sum(counts.values())
  77. print(f"counted {n:,} tokens, {len(counts):,} distinct, {time.perf_counter()-t0:.0f}s", flush=True)
  78.  
  79. t1 = time.perf_counter()
  80. with open(dst, 'wb') as out:
  81. for tok in sorted(counts): # byte order == LC_ALL=C
  82. out.write((tok + b'\n') * counts[tok])
  83. print(f"wrote sorted output {time.perf_counter()-t1:.0f}s; total {time.perf_counter()-t0:.0f}s", flush=True)
  84.  
  85. ## 4. Counting sort, Rust (5.7 min). cargo build --release; count_sort.exe corpus.txt out.txt
  86.  
  87. ### Cargo.toml
  88. [package]
  89. name = "count_sort"
  90. version = "0.1.0"
  91. edition = "2021"
  92.  
  93. [profile.release]
  94. opt-level = 3
  95. lto = true
  96. codegen-units = 1
  97.  
  98. ### src/main.rs
  99. // Counting sort for a token corpus: count every whitespace-separated token in one pass,
  100. // sort the (small) vocabulary, write each token count times. Output is byte-identical to
  101. // `tr ' ' '\n' < corpus | LC_ALL=C sort`.
  102. use std::collections::HashMap;
  103. use std::io::{BufWriter, Read, Write};
  104. use std::time::Instant;
  105.  
  106. fn main() {
  107. let args: Vec<String> = std::env::args().collect();
  108. let (src, dst) = (&args[1], &args[2]);
  109. let t0 = Instant::now();
  110.  
  111. let mut counts: HashMap<Box<[u8]>, u64> = HashMap::with_capacity(1 << 20);
  112. let mut f = std::fs::File::open(src).unwrap();
  113. let mut buf = vec![0u8; 1 << 27];
  114. let mut start = 0usize; // bytes carried over from previous chunk (partial token)
  115. let mut total: u64 = 0;
  116. loop {
  117. let n = f.read(&mut buf[start..]).unwrap();
  118. if n == 0 { break; }
  119. let end = start + n;
  120. // last whitespace boundary; everything after is a partial token to carry over
  121. let cut = buf[..end].iter().rposition(|&b| b == b' ' || b == b'\n').map_or(0, |i| i + 1);
  122. for tok in buf[..cut].split(|&b| b == b' ' || b == b'\n').filter(|t| !t.is_empty()) {
  123. total += 1;
  124. match counts.get_mut(tok) {
  125. Some(c) => *c += 1,
  126. None => { counts.insert(tok.into(), 1); }
  127. }
  128. }
  129. buf.copy_within(cut..end, 0);
  130. start = end - cut;
  131. }
  132. for tok in buf[..start].split(|&b| b == b' ' || b == b'\n').filter(|t| !t.is_empty()) {
  133. total += 1;
  134. *counts.entry(tok.into()).or_insert(0) += 1;
  135. }
  136. eprintln!("counted {} tokens, {} distinct, {:.1}s", total, counts.len(), t0.elapsed().as_secs_f64());
  137.  
  138. let t1 = Instant::now();
  139. let mut keys: Vec<(&Box<[u8]>, &u64)> = counts.iter().collect();
  140. keys.sort_unstable_by(|a, b| a.0.cmp(b.0)); // byte order == LC_ALL=C
  141. let mut out = BufWriter::with_capacity(1 << 24, std::fs::File::create(dst).unwrap());
  142. let mut line = Vec::new();
  143. for (tok, &c) in keys {
  144. line.clear(); line.extend_from_slice(tok); line.push(b'\n');
  145. for _ in 0..c { out.write_all(&line).unwrap(); }
  146. }
  147. out.flush().unwrap();
  148. eprintln!("wrote sorted output {:.1}s; total {:.1}s", t1.elapsed().as_secs_f64(), t0.elapsed().as_secs_f64());
  149. }
  150.  
  151. ## Results: all three outputs byte-identical (cmp). 6,061,752,179 lines, sort -c passes.
Advertisement
Add Comment
Please, Sign In to add comment