सी ++ (एक ला नूथ)
मैं उत्सुक था कि नुथ का कार्यक्रम कैसा होगा, इसलिए मैंने उसका (मूल रूप से पास्कल) कार्यक्रम C ++ में अनुवादित किया।
भले ही नुथ का प्राथमिक लक्ष्य गति नहीं था, लेकिन साक्षर प्रोग्रामिंग के अपने वेब सिस्टम को स्पष्ट करने के लिए, कार्यक्रम आश्चर्यजनक रूप से प्रतिस्पर्धी है, और अब तक के किसी भी उत्तर की तुलना में तेजी से समाधान की ओर जाता है। यहाँ उनके कार्यक्रम के अनुवाद (WEB कार्यक्रम के संबंधित "खंड" संख्या " {§24}
" जैसे टिप्पणियों में उल्लिखित हैं ):
#include <iostream>
#include <cassert>
// Adjust these parameters based on input size.
const int TRIE_SIZE = 800 * 1000; // Size of the hash table used for the trie.
const int ALPHA = 494441; // An integer that's approximately (0.61803 * TRIE_SIZE), and relatively prime to T = TRIE_SIZE - 52.
const int kTolerance = TRIE_SIZE / 100; // How many places to try, to find a new place for a "family" (=bunch of children).
typedef int32_t Pointer; // [0..TRIE_SIZE), an index into the array of Nodes
typedef int8_t Char; // We only care about 1..26 (plus two values), but there's no "int5_t".
typedef int32_t Count; // The number of times a word has been encountered.
// These are 4 separate arrays in Knuth's implementation.
struct Node {
Pointer link; // From a parent node to its children's "header", or from a header back to parent.
Pointer sibling; // Previous sibling, cyclically. (From smallest child to header, and header to largest child.)
Count count; // The number of times this word has been encountered.
Char ch; // EMPTY, or 1..26, or HEADER. (For nodes with ch=EMPTY, the link/sibling/count fields mean nothing.)
} node[TRIE_SIZE + 1];
// Special values for `ch`: EMPTY (free, can insert child there) and HEADER (start of family).
const Char EMPTY = 0, HEADER = 27;
const Pointer T = TRIE_SIZE - 52;
Pointer x; // The `n`th time we need a node, we'll start trying at x_n = (alpha * n) mod T. This holds current `x_n`.
// A header can only be in T (=TRIE_SIZE-52) positions namely [27..TRIE_SIZE-26].
// This transforms a "h" from range [0..T) to the above range namely [27..T+27).
Pointer rerange(Pointer n) {
n = (n % T) + 27;
// assert(27 <= n && n <= TRIE_SIZE - 26);
return n;
}
// Convert trie node to string, by walking up the trie.
std::string word_for(Pointer p) {
std::string word;
while (p != 0) {
Char c = node[p].ch; // assert(1 <= c && c <= 26);
word = static_cast<char>('a' - 1 + c) + word;
// assert(node[p - c].ch == HEADER);
p = (p - c) ? node[p - c].link : 0;
}
return word;
}
// Increment `x`, and declare `h` (the first position to try) and `last_h` (the last position to try). {§24}
#define PREPARE_X_H_LAST_H x = (x + ALPHA) % T; Pointer h = rerange(x); Pointer last_h = rerange(x + kTolerance);
// Increment `h`, being careful to account for `last_h` and wraparound. {§25}
#define INCR_H { if (h == last_h) { std::cerr << "Hit tolerance limit unfortunately" << std::endl; exit(1); } h = (h == TRIE_SIZE - 26) ? 27 : h + 1; }
// `p` has no children. Create `p`s family of children, with only child `c`. {§27}
Pointer create_child(Pointer p, int8_t c) {
// Find `h` such that there's room for both header and child c.
PREPARE_X_H_LAST_H;
while (!(node[h].ch == EMPTY and node[h + c].ch == EMPTY)) INCR_H;
// Now create the family, with header at h and child at h + c.
node[h] = {.link = p, .sibling = h + c, .count = 0, .ch = HEADER};
node[h + c] = {.link = 0, .sibling = h, .count = 0, .ch = c};
node[p].link = h;
return h + c;
}
// Move `p`'s family of children to a place where child `c` will also fit. {§29}
void move_family_for(const Pointer p, Char c) {
// Part 1: Find such a place: need room for `c` and also all existing children. {§31}
PREPARE_X_H_LAST_H;
while (true) {
INCR_H;
if (node[h + c].ch != EMPTY) continue;
Pointer r = node[p].link;
int delta = h - r; // We'd like to move each child by `delta`
while (node[r + delta].ch == EMPTY and node[r].sibling != node[p].link) {
r = node[r].sibling;
}
if (node[r + delta].ch == EMPTY) break; // There's now space for everyone.
}
// Part 2: Now actually move the whole family to start at the new `h`.
Pointer r = node[p].link;
int delta = h - r;
do {
Pointer sibling = node[r].sibling;
// Move node from current position (r) to new position (r + delta), and free up old position (r).
node[r + delta] = {.ch = node[r].ch, .count = node[r].count, .link = node[r].link, .sibling = node[r].sibling + delta};
if (node[r].link != 0) node[node[r].link].link = r + delta;
node[r].ch = EMPTY;
r = sibling;
} while (node[r].ch != EMPTY);
}
// Advance `p` to its `c`th child. If necessary, add the child, or even move `p`'s family. {§21}
Pointer find_child(Pointer p, Char c) {
// assert(1 <= c && c <= 26);
if (p == 0) return c; // Special case for first char.
if (node[p].link == 0) return create_child(p, c); // If `p` currently has *no* children.
Pointer q = node[p].link + c;
if (node[q].ch == c) return q; // Easiest case: `p` already has a `c`th child.
// Make sure we have room to insert a `c`th child for `p`, by moving its family if necessary.
if (node[q].ch != EMPTY) {
move_family_for(p, c);
q = node[p].link + c;
}
// Insert child `c` into `p`'s family of children (at `q`), with correct siblings. {§28}
Pointer h = node[p].link;
while (node[h].sibling > q) h = node[h].sibling;
node[q] = {.ch = c, .count = 0, .link = 0, .sibling = node[h].sibling};
node[h].sibling = q;
return q;
}
// Largest descendant. {§18}
Pointer last_suffix(Pointer p) {
while (node[p].link != 0) p = node[node[p].link].sibling;
return p;
}
// The largest count beyond which we'll put all words in the same (last) bucket.
// We do an insertion sort (potentially slow) in last bucket, so increase this if the program takes a long time to walk trie.
const int MAX_BUCKET = 10000;
Pointer sorted[MAX_BUCKET + 1]; // The head of each list.
// Records the count `n` of `p`, by inserting `p` in the list that starts at `sorted[n]`.
// Overwrites the value of node[p].sibling (uses the field to mean its successor in the `sorted` list).
void record_count(Pointer p) {
// assert(node[p].ch != HEADER);
// assert(node[p].ch != EMPTY);
Count f = node[p].count;
if (f == 0) return;
if (f < MAX_BUCKET) {
// Insert at head of list.
node[p].sibling = sorted[f];
sorted[f] = p;
} else {
Pointer r = sorted[MAX_BUCKET];
if (node[p].count >= node[r].count) {
// Insert at head of list
node[p].sibling = r;
sorted[MAX_BUCKET] = p;
} else {
// Find right place by count. This step can be SLOW if there are too many words with count >= MAX_BUCKET
while (node[p].count < node[node[r].sibling].count) r = node[r].sibling;
node[p].sibling = node[r].sibling;
node[r].sibling = p;
}
}
}
// Walk the trie, going over all words in reverse-alphabetical order. {§37}
// Calls "record_count" for each word found.
void walk_trie() {
// assert(node[0].ch == HEADER);
Pointer p = node[0].sibling;
while (p != 0) {
Pointer q = node[p].sibling; // Saving this, as `record_count(p)` will overwrite it.
record_count(p);
// Move down to last descendant of `q` if any, else up to parent of `q`.
p = (node[q].ch == HEADER) ? node[q].link : last_suffix(q);
}
}
int main(int, char** argv) {
// Program startup
std::ios::sync_with_stdio(false);
// Set initial values {§19}
for (Char i = 1; i <= 26; ++i) node[i] = {.ch = i, .count = 0, .link = 0, .sibling = i - 1};
node[0] = {.ch = HEADER, .count = 0, .link = 0, .sibling = 26};
// read in file contents
FILE *fptr = fopen(argv[1], "rb");
fseek(fptr, 0L, SEEK_END);
long dataLength = ftell(fptr);
rewind(fptr);
char* data = (char*)malloc(dataLength);
fread(data, 1, dataLength, fptr);
if (fptr) fclose(fptr);
// Loop over file contents: the bulk of the time is spent here.
Pointer p = 0;
for (int i = 0; i < dataLength; ++i) {
Char c = (data[i] | 32) - 'a' + 1; // 1 to 26, for 'a' to 'z' or 'A' to 'Z'
if (1 <= c && c <= 26) {
p = find_child(p, c);
} else {
++node[p].count;
p = 0;
}
}
node[0].count = 0;
walk_trie();
const int max_words_to_print = atoi(argv[2]);
int num_printed = 0;
for (Count f = MAX_BUCKET; f >= 0 && num_printed <= max_words_to_print; --f) {
for (Pointer p = sorted[f]; p != 0 && num_printed < max_words_to_print; p = node[p].sibling) {
std::cout << word_for(p) << " " << node[p].count << std::endl;
++num_printed;
}
}
return 0;
}
नुथ के कार्यक्रम से अंतर:
- मैं नुथ के 4 सरणियों संयुक्त
link
, sibling
, count
और ch
एक की एक सरणी मेंstruct Node
(यह आसान इस तरह से समझने के लिए लगता है)।
- मैंने साक्षर-प्रोग्रामिंग (WEB- शैली) को अनुभागों के अधिक पारंपरिक फ़ंक्शन कॉल (और मैक्रोज़ के एक जोड़े) में अनुभागीय परिवर्तन को बदल दिया।
- हमें मानक पास्कल के अजीब I / O सम्मेलनों / प्रतिबंधों का उपयोग करने की आवश्यकता नहीं है, इसलिए उपयोग करना
fread
औरdata[i] | 32 - 'a'
अन्य उत्तर यहाँ के रूप में, पास्कल वैकल्पिक हल के बजाय।
- यदि कार्यक्रम चल रहा है, तो हम सीमा से अधिक (अंतरिक्ष से बाहर), नथ के मूल कार्यक्रम बाद के शब्दों को छोड़ने और अंत में एक संदेश मुद्रित करने के लिए इसे शान से करते हैं। (यह कहना बिलकुल सही नहीं है कि मैक्लीरो ने "नथ के समाधान की आलोचना की, क्योंकि वह बाइबल के पूर्ण पाठ को संसाधित करने में सक्षम नहीं थे"; वह केवल इस ओर इशारा कर रहा था कि कभी-कभी किसी पाठ में बहुत देर हो सकती है, जैसे कि यीशु शब्द "बाइबल में, इसलिए त्रुटि की स्थिति सहज नहीं है।) मैंने नोइज़ियर (और वैसे भी आसान) कार्यक्रम को समाप्त करने के दृष्टिकोण को लिया है।
- कार्यक्रम स्मृति उपयोग को नियंत्रित करने के लिए एक निरंतर TRIE_SIZE घोषित करता है, जिसे मैंने टक्कर दी थी। (मूल आवश्यकताओं के लिए 32767 का स्थिरांक चुना गया था - "एक उपयोगकर्ता को बीस-पृष्ठ तकनीकी पेपर में 100 सबसे लगातार शब्दों को खोजने में सक्षम होना चाहिए (लगभग 50K बाइट फ़ाइल)" और क्योंकि पास्कल पूर्णांक पूर्णांक के साथ अच्छी तरह से व्यवहार करता है। प्रकार और उन्हें बेहतर तरीके से पैक करते हैं। हमें इसे 25x से 800,000 तक बढ़ाना पड़ा क्योंकि परीक्षण इनपुट अब 20 मिलियन गुना बड़ा है।)
- स्ट्रिंग्स के अंतिम मुद्रण के लिए, हम बस ट्राइ कर सकते हैं और एक गूंगा (संभवतः द्विघात) स्ट्रिंग एपेंड भी कर सकते हैं।
इसके अलावा, यह हूबहू नुथ के कार्यक्रम (अपने हैश ट्राई / पैक्ड ट्राई डेटा स्ट्रक्चर और बकेट सॉर्ट का उपयोग करके) के बिल्कुल समान है, और इनपुट में सभी पात्रों के माध्यम से लूप करते समय बहुत अधिक संचालन (जैसा कि नुथ का पास्कल कार्यक्रम होगा) करता है; ध्यान दें कि यह कोई बाहरी एल्गोरिथ्म या डेटा संरचना पुस्तकालयों का उपयोग नहीं करता है, और यह भी कि समान आवृत्ति के शब्द वर्णमाला के क्रम में मुद्रित किए जाएंगे।
समय
के साथ संकलित किया
clang++ -std=c++17 -O2 ptrie-walktrie.cc
जब यहां सबसे बड़े टेस्टकेस पर चला जाता है ( giganovel
100,000 शब्दों के साथ अनुरोध किया जाता है), और यहां अब तक पोस्ट किए गए सबसे तेज कार्यक्रम की तुलना में, मुझे यह थोड़ा या लगातार तेज लगता है:
target/release/frequent: 4.809 ± 0.263 [ 4.45.. 5.62] [... 4.63 ... 4.75 ... 4.88...]
ptrie-walktrie: 4.547 ± 0.164 [ 4.35.. 4.99] [... 4.42 ... 4.5 ... 4.68...]
(शीर्ष पंक्ति एंडर्स कसेर्ग के जंग समाधान है; नीचे का उपरोक्त कार्यक्रम है। ये मीन, अधिकतम, मध्य और चतुर्थक के साथ 100 रन से समयावधि हैं।)
विश्लेषण
यह तेज क्यों है? ऐसा नहीं है कि C ++ रस्ट की तुलना में तेज़ है, या कि नुथ का कार्यक्रम सबसे तेज़ संभव है - वास्तव में, नथुथ का कार्यक्रम आवेषण पर (जैसा कि वह उल्लेख करता है) ट्राइ-पैकिंग (मेमोरी को संरक्षित करने के लिए) के कारण धीमा है। कारण, मुझे संदेह है, कुछ इस से संबंधित है कि नूथ ने 2008 में शिकायत की थी :
64-बिट पॉइंटर्स के बारे में एक ज्वाला
जब मैंने 4 गीगाबाइट से कम रैम का उपयोग करने वाले प्रोग्राम को संकलित किया, तो 64-बिट पॉइंटर्स होना बिल्कुल मुहावरा है। जब इस तरह के पॉइंटर मान किसी संरचना के अंदर दिखाई देते हैं, तो वे न केवल आधी मेमोरी बर्बाद करते हैं, वे प्रभावी रूप से कैश के आधे भाग को फेंक देते हैं।
ऊपर दिए गए कार्यक्रम में 32-बिट ऐरे इंडिकेशंस (64-बिट पॉइंटर्स नहीं) का उपयोग किया गया है, इसलिए "नोड" संरचना कम मेमोरी में रहती है, इसलिए स्टैक पर अधिक नोड्स और कम कैश मिस हैं। (वास्तव में, वहाँ था कुछ काम के रूप में इस पर x32 ABI , लेकिन यह प्रतीत हो रहा है एक अच्छी स्थिति में नहीं है, भले ही विचार स्पष्ट रूप से उपयोगी है, उदाहरण के लिए देखने के हाल ही में घोषणा की वी 8 में सूचक संपीड़न । ओह अच्छा।) तो पर giganovel
, यह प्रोग्राम (पैक्ड) ट्राइ के लिए 12.8 एमबी का उपयोग करता है, बनाम रस्ट प्रोग्राम के 32.18 एमबी के लिए इसके ट्राइ (ऑन giganovel
) के लिए। हम 1000x ("गिगोनवेल" से "टेरानोवेल" तक) कह सकते हैं और अभी भी 32-बिट सूचकांकों से अधिक नहीं हो सकते हैं, इसलिए यह एक उचित विकल्प लगता है।
तेज़ वैरिएंट
हम गति के लिए अनुकूलन कर सकते हैं और पैकिंग को आगे बढ़ा सकते हैं, इसलिए हम वास्तव में (नॉन-पैक) ट्राइ का उपयोग रस्ट समाधान में कर सकते हैं, सूचक के बजाय सूचक के साथ। यह कुछ ऐसा है जो तेजी से होता है और विभिन्न शब्दों, वर्णों आदि की संख्या पर कोई पूर्व-निर्धारित सीमा नहीं है:
#include <iostream>
#include <cassert>
#include <vector>
#include <algorithm>
typedef int32_t Pointer; // [0..node.size()), an index into the array of Nodes
typedef int32_t Count;
typedef int8_t Char; // We'll usually just have 1 to 26.
struct Node {
Pointer link; // From a parent node to its children's "header", or from a header back to parent.
Count count; // The number of times this word has been encountered. Undefined for header nodes.
};
std::vector<Node> node; // Our "arena" for Node allocation.
std::string word_for(Pointer p) {
std::vector<char> drow; // The word backwards
while (p != 0) {
Char c = p % 27;
drow.push_back('a' - 1 + c);
p = (p - c) ? node[p - c].link : 0;
}
return std::string(drow.rbegin(), drow.rend());
}
// `p` has no children. Create `p`s family of children, with only child `c`.
Pointer create_child(Pointer p, Char c) {
Pointer h = node.size();
node.resize(node.size() + 27);
node[h] = {.link = p, .count = -1};
node[p].link = h;
return h + c;
}
// Advance `p` to its `c`th child. If necessary, add the child.
Pointer find_child(Pointer p, Char c) {
assert(1 <= c && c <= 26);
if (p == 0) return c; // Special case for first char.
if (node[p].link == 0) return create_child(p, c); // Case 1: `p` currently has *no* children.
return node[p].link + c; // Case 2 (easiest case): Already have the child c.
}
int main(int, char** argv) {
auto start_c = std::clock();
// Program startup
std::ios::sync_with_stdio(false);
// read in file contents
FILE *fptr = fopen(argv[1], "rb");
fseek(fptr, 0, SEEK_END);
long dataLength = ftell(fptr);
rewind(fptr);
char* data = (char*)malloc(dataLength);
fread(data, 1, dataLength, fptr);
fclose(fptr);
node.reserve(dataLength / 600); // Heuristic based on test data. OK to be wrong.
node.push_back({0, 0});
for (Char i = 1; i <= 26; ++i) node.push_back({0, 0});
// Loop over file contents: the bulk of the time is spent here.
Pointer p = 0;
for (long i = 0; i < dataLength; ++i) {
Char c = (data[i] | 32) - 'a' + 1; // 1 to 26, for 'a' to 'z' or 'A' to 'Z'
if (1 <= c && c <= 26) {
p = find_child(p, c);
} else {
++node[p].count;
p = 0;
}
}
++node[p].count;
node[0].count = 0;
// Brute-force: Accumulate all words and their counts, then sort by frequency and print.
std::vector<std::pair<int, std::string>> counts_words;
for (Pointer i = 1; i < static_cast<Pointer>(node.size()); ++i) {
int count = node[i].count;
if (count == 0 || i % 27 == 0) continue;
counts_words.push_back({count, word_for(i)});
}
auto cmp = [](auto x, auto y) {
if (x.first != y.first) return x.first > y.first;
return x.second < y.second;
};
std::sort(counts_words.begin(), counts_words.end(), cmp);
const int max_words_to_print = std::min<int>(counts_words.size(), atoi(argv[2]));
for (int i = 0; i < max_words_to_print; ++i) {
auto [count, word] = counts_words[i];
std::cout << word << " " << count << std::endl;
}
return 0;
}
यह कार्यक्रम, यहां समाधानों की तुलना में बहुत कुछ करने के लिए giganovel
पर्याप्त है, बावजूद इसके ट्राइ के लिए केवल 12.2MB का उपयोग करता है (और ) तेज होने का प्रबंधन करता है। इस कार्यक्रम की समयसीमा (अंतिम पंक्ति), पहले बताए गए समयों की तुलना में:
target/release/frequent: 4.809 ± 0.263 [ 4.45.. 5.62] [... 4.63 ... 4.75 ... 4.88...]
ptrie-walktrie: 4.547 ± 0.164 [ 4.35.. 4.99] [... 4.42 ... 4.5 ... 4.68...]
itrie-nolimit: 3.907 ± 0.127 [ 3.69.. 4.23] [... 3.81 ... 3.9 ... 4.0...]
मैं यह देखने के लिए उत्सुक हूं कि यह (या हैश-ट्राइ प्रोग्राम) क्या होगा अगर रुस्त में अनुवाद किया जाए । :-)
आगे की जानकारी
यहां उपयोग किए गए डेटा संरचना के बारे में: TAOCP के खंड 3 में धारा 6.3 (डिजिटल खोज, अर्थात कोशिश) के एक्सरसाइज 4 में "पैकिंग" की कोशिशों का एक विवरण दिया गया है, और टीएक्स में शिफॉन के छात्र फ्रैंक लिआंग की थीसिस के बारे में भी बताया गया है : कॉम-पुट-एर द्वारा शब्द हाय-फिन-ए-टियोन ।
बेंटले के कॉलम, नूथ के कार्यक्रम, और मैकलरॉय की समीक्षा (यूनिक्स दर्शन के बारे में केवल एक छोटा सा हिस्सा) का संदर्भ पिछले और बाद के स्तंभों के प्रकाश में स्पष्ट है , और नॉथ का पिछला अनुभव संकलक, टीएआरसीपी और टीएक्स सहित है।
प्रोग्रामिंग स्टाइल में एक पूरी पुस्तक एक्सरसाइज है , जो इस विशेष कार्यक्रम के विभिन्न दृष्टिकोण दिखाती है, आदि।
मेरे पास ऊपर के बिंदुओं पर विस्तृत एक अधूरा ब्लॉग पोस्ट है; ऐसा होने पर इस उत्तर को संपादित कर सकते हैं। इस बीच, नुत के जन्मदिन के अवसर (10 जनवरी) पर, वैसे भी यहाँ इस उत्तर को पोस्ट करना। :-)