Winnowing — Document Fingerprinting
Weight: 0.9
What It Does
Winnowing creates a set of fingerprints from a document — a compressed representation that survives local edits, insertion, and deletion. It’s the core algorithm behind MOSS (Measure of Software Similarity), used by thousands of universities worldwide.
How It Works
Step 1: Create k-grams
Slide a window of size k over the token sequence:
Tokens: [int, main, return, 0, end, print, hello, world]
3-grams: [int main return], [main return 0], [return 0 end], ...
Step 2: Hash each k-gram
Compute a hash for each k-gram using a polynomial hash function (Analogi uses FNV-1a):
h₁ = hash("int main return") = 7234801...
h₂ = hash("main return 0") = 1829412...
h₃ = hash("return 0 end") = 9012341...
...
Step 3: Winnow — select fingerprints
Slide a window of size w over the hash sequence. In each window, select the minimum hash value (rightmost on tie):
Window size = 4
Hashes: [72, 18, 90, 45, 23, 67, 11, 89, ...]
Window [72, 18, 90, 45] → select 18 ✓
Window [18, 90, 45, 23] → select 18 (same position, skip)
Window [90, 45, 23, 67] → select 23 ✓
Window [45, 23, 67, 11] → select 11 ✓
The selected hashes are the document’s fingerprints.
Step 4: Compare fingerprint sets
Compare two documents using Jaccard similarity on their fingerprint sets:
J(A, B) = |A ∩ B| / |A ∪ B|
Why Winnowing?
| Property | Explanation |
|---|---|
| Noise threshold | Matches shorter than k tokens are not detected (filters language boilerplate) |
| Guaranteed detection | Any match of length ≥ k + w - 1 tokens is guaranteed to be detected |
| Local edit resistance | Inserting or deleting a few tokens only affects nearby fingerprints |
| Position independence | Reordering code blocks is still detected (shared fingerprints persist) |
Parameters in Analogi
| Parameter | Value | Effect |
|---|---|---|
| k (kgram size) | 5 | Matches < 5 tokens are noise |
| w (window size) | 4 | Guarantees detection of matches ≥ 8 tokens |
| Threshold | 0.3 | Reports pairs with > 30% fingerprint overlap |
Example: Why Renaming Variables Doesn’t Help
After preprocessing normalizes identifiers:
Original: int fibonacci(int n) { ... }
Renamed: int calculate(int x) { ... }
Normalized: int v0(int v1) { ... } ← same for both!
Both produce identical k-grams → identical fingerprints → similarity = 1.0.
Example: Why Reordering Partially Helps
If a student moves a function from the top to the bottom of the file:
- The k-grams within the function are unchanged → those fingerprints still match
- Only k-grams that span the boundary are different
- Result: still high similarity, but not 100%
Limitations
- Very short files produce few fingerprints (unreliable)
- Adding many new lines of code (padding) dilutes the overlap percentage
- That’s why Analogi pairs winnowing with cosine similarity (which handles the “vocabulary” dimension)
Further Reading
- Schleimer, S., Wilkerson, D.S., & Aiken, A. (2003). “Winnowing: Local Algorithms for Document Fingerprinting.” ACM SIGMOD.
- MOSS homepage