সি ++ (একটি লা নুথ)
আমি কৌতূহল ছিল কীভাবে নুথের প্রোগ্রামটি ভাড়া নিবে, তাই আমি তার (মূলত পাস্কাল) প্রোগ্রামটি সি ++ তে অনুবাদ করেছিলাম।
যদিও নুথের প্রাথমিক লক্ষ্যটি গতি ছিল না তবে তার WEB সাক্ষরিত প্রোগ্রামিংয়ের চিত্রটি তুলে ধরার জন্য, প্রোগ্রামটি আশ্চর্যরকমভাবে প্রতিযোগিতামূলক এবং এখানকার উত্তরগুলির তুলনায় একটি দ্রুত সমাধানের দিকে নিয়ে যায়। এখানে তার প্রোগ্রামটির আমার অনুবাদ (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;
}
নথের প্রোগ্রাম থেকে পার্থক্য:
- আমি Knuth এর 4 অ্যারে মিলিত
link
, sibling
, count
এবং ch
একটি একটি অ্যারের মধ্যেstruct Node
(এটা সহজ এই ভাবে বুঝতে এটি)।
- আমি সাক্ষরতা-প্রোগ্রামিং (ডাব্লুইইবি-স্টাইল) বিভাগগুলির পাঠ্যগত ট্রান্সকোসিয়েশনকে আরও প্রচলিত ফাংশন কলগুলিতে (এবং কয়েকজন ম্যাক্রো) পরিবর্তন করেছি।
- আমাদের স্ট্যান্ডার্ড পাস্কালের অদ্ভুত আই / ও কনভেনশন / বিধিনিষেধ ব্যবহার করার দরকার নেই, সুতরাং ব্যবহার করে
fread
এবংdata[i] | 32 - 'a'
অন্যান্য উত্তর এখানে হিসাবে, পাসকাল কার্যসংক্রান্ত পরিবর্তে।
- প্রোগ্রামটি চলাকালীন আমরা সীমা ছাড়িয়ে (স্থান ছাড়িয়ে যায়) ক্ষেত্রে, নুথের আসল প্রোগ্রামটি পরে শব্দগুলি বাদ দিয়ে এবং শেষে একটি বার্তা মুদ্রণের মাধ্যমে এটিকে মনোনিবেশ করে। (এটা বলা ঠিক নয় যে ম্যাকিলারোই "বাইবেলের সম্পূর্ণ পাঠ্য প্রক্রিয়া করতে না পেরে নূথের সমাধানের সমালোচনা করেছিলেন"; তিনি কেবল ইঙ্গিত করছিলেন যে মাঝে মাঝে ঘন ঘন শব্দগুলি কোনও পাঠ্যে খুব দেরীতে আসতে পারে, যেমন "যিশু শব্দ" "বাইবেলে, সুতরাং ত্রুটির শঙ্কা নিস্পাপ নয়)) আমি প্রোগ্রামটি সহজভাবে বন্ধ করার মত উচ্চস্বরে (এবং যাইহোক সহজ) দৃষ্টিভঙ্গি নিয়েছি।
- প্রোগ্রামটি মেমোরির ব্যবহারটি নিয়ন্ত্রণ করতে একটি ধ্রুবক TRIE_SIZE ঘোষণা করে, যা আমি ধাক্কা দিয়েছি। (32767 এর ধ্রুবকটি মূল প্রয়োজনীয়তার জন্য বেছে নেওয়া হয়েছিল - "কোনও ব্যবহারকারীকে বিশ পৃষ্ঠার প্রযুক্তিগত কাগজে (প্রায় 50K বাইট ফাইলের মধ্যে প্রায় 100 টি ঘন ঘন শব্দগুলি খুঁজে পেতে সক্ষম হওয়া উচিত") এবং কারণ পাস্কেলটি পূর্ণসংখ্যার পূর্ণসংখ্যার সাথে ভালভাবে আচরণ করে " টাইপ করুন এবং এগুলি সর্বোত্তমভাবে প্যাক করে test পরীক্ষার ইনপুট এখন 20 মিলিয়ন গুণ বেশি বড় হওয়ায় আমাদের এটিকে 25x থেকে 800,000 বাড়াতে হয়েছিল)
- স্ট্রিংগুলির চূড়ান্ত মুদ্রণের জন্য, আমরা কেবল ত্রি ট্রাই করতে পারি এবং একটি বোবা (সম্ভবত চতুষ্পদ এমনকি স্ট্রিং সংযোজন) করতে পারি।
এগুলি ছাড়াও, এটি হুবহু নুথের প্রোগ্রাম (তার হ্যাশ ট্রাই / প্যাকড ট্রাই ডেটা স্ট্রাকচার এবং বাল্টিট সাজ্ট ব্যবহার করে), এবং ইনপুটে সমস্ত অক্ষরকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফাঁকে ফোটার সময় (হুথ ট্রাই / প্যাকড ট্রাই ডাটা স্ট্রাকচার এবং বাল্টিট সাজ্ট) ব্যবহার করে এটি ঠিক একই কাজ করে; মনে রাখবেন যে এটি কোনও বাহ্যিক অ্যালগরিদম বা ডেটা স্ট্রাকচার লাইব্রেরি ব্যবহার করে না এবং সমান ফ্রিকোয়েন্সি শব্দগুলি বর্ণানুক্রমিকভাবে মুদ্রিত হবে।
টাইমিং
সংকলিত
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...]
(শীর্ষস্থানটি হ'ল এন্ডারস ক্যাসরগের জাস্ট সলিউশন; নীচের অংশটি উপরের প্রোগ্রামটি These এগুলি গড়, মিনিট, সর্বাধিক, মধ্যম এবং কোয়ার্টাইলস সহ 100 রানের সময়)
বিশ্লেষণ
এত দ্রুত কেন? এটি যে জাস্টের তুলনায় সি ++ তত দ্রুত নয় বা নথের প্রোগ্রামটি সবচেয়ে দ্রুত সম্ভব - প্রকৃতপক্ষে, ট্রাই-প্যাকিংয়ের (স্মৃতি সংরক্ষণের জন্য) নুথের প্রোগ্রাম সন্নিবেশগুলিতে (যেমন তিনি উল্লেখ করেছেন) ধীর। আমার সন্দেহ, কারণটি 2008 সালে নথের অভিযোগের সাথে সম্পর্কিত ছিল :
Fla৪-বিট পয়েন্টার সম্পর্কে একটি শিখা
4 গিগাবাইটের চেয়ে কম র্যাম ব্যবহার করা একটি প্রোগ্রাম সংকলন করার সময় 64৪-বিট পয়েন্টার থাকা একেবারে বোকামি। যখন এই জাতীয় নির্দেশকের মানগুলি কোনও কাঠামোর ভিতরে উপস্থিত হয়, তখন তারা কেবল অর্ধেক স্মৃতি নষ্ট করে না, তারা কার্যকরভাবে ক্যাশের অর্ধেক ফেলে দেয়।
উপরের প্রোগ্রামটি 32-বিট অ্যারে সূচকগুলি ব্যবহার করে (64-বিট পয়েন্টার নয়), সুতরাং "নোড" স্ট্রাক্টটি কম মেমরি দখল করে, তাই স্ট্যাকের উপর আরও নোড রয়েছে এবং কম ক্যাশে মিস হয়েছে। (আসলে, সেখানে ছিল কিছু কাজ যেমন এই x32 ABI- র , কিন্তু এটা মনে করা হয় একটি ভাল রাষ্ট্র নেই যদিও ধারণা স্পষ্টত দরকারী, যেমন দেখতে সাম্প্রতিক ঘোষণা এর V8 মধ্যে পয়েন্টার কম্প্রেশন । ওহ ভাল।) সুতরাং 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 ব্যবহার করে (এবং ) দ্রুততর করে তোলে be উল্লিখিত আগের সময়ের সাথে তুলনা করে এই প্রোগ্রামের (শেষ লাইন) সময়সীমা:
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...]
মরিচায় অনুবাদ হলে এটি (বা হ্যাশ-ট্রাই প্রোগ্রাম) কী পছন্দ করবে তা দেখার জন্য আমি আগ্রহী । :-)
অধিকতর বিস্তারিত
এখানে ব্যবহৃত ডেটা স্ট্রাকচার সম্পর্কে: "প্যাকিং" চেষ্টাগুলির ব্যাখ্যা টিএওসিপি-র খণ্ড ৩ য় বিভাগের Digital.৩ (ডিজিটাল অনুসন্ধান, অর্থাত্ চেষ্টা করা) অনুশীলন এবং টেক্স-এর হাইফেনেশন সম্পর্কে নুথের ছাত্র ফ্র্যাঙ্ক লিয়াংয়ের থিসিসেও বর্ণিত হয়েছে : কম হাই-ফেন-এ-টিওন শব্দটি কম-পুট-এর দ্বারা লিখেছেন ।
বেন্টলির কলাম, নুথের প্রোগ্রাম এবং ম্যাকিলারয়ের পর্যালোচনা (যার কেবলমাত্র একটি ছোট অংশই ইউনিক্স দর্শনের বিষয়ে ছিল) এর পূর্ববর্তী এবং পরবর্তী কলামগুলির আলোকে আরও পরিষ্কার করা হয়েছে এবং সংকলক, টিএওসিপি এবং টেক্স সহ নুথের পূর্বের অভিজ্ঞতা।
প্রোগ্রামিং শৈলীতে এক্সারসাইজগুলিতে একটি সম্পূর্ণ বই রয়েছে , এই নির্দিষ্ট প্রোগ্রামের জন্য বিভিন্ন পদ্ধতির ইত্যাদি দেখায় etc.
আমার উপরের পয়েন্টগুলিতে একটি অসম্পূর্ণ ব্লগ পোস্ট রয়েছে; এটি শেষ হয়ে গেলে এই উত্তরটি সম্পাদনা করতে পারে। এদিকে, নুথের জন্মদিন উপলক্ষে (10 জানুয়ারী) যাইহোক, এই উত্তর এখানে পোস্ট করা। :-)