apeescape2.com
  • メイン
  • 設計プロセス
  • バックエンド
  • アジャイル
  • 計画と予測
バックエンド

ガベージコレクターの排除:RAIIウェイ

最初はCがありました。Cには、静的、自動、動的の3種類のメモリ割り当てがあります。静的変数はソースファイルに埋め込まれている定数であり、サイズは既知であり、変更されることはないため、それほど興味深いものではありません。自動割り当ては、スタック割り当てと考えることができます。スペースは、字句ブロックに入るときに割り当てられ、そのブロックが出るときに解放されます。その最も重要な機能はそれに直接関連しています。 C99までは、自動的に割り当てられた変数は、コンパイル時にサイズがわかっている必要がありました。これは、文字列、リスト、マップ、およびこれらから派生した構造が、動的メモリ内のヒープ上に存在する必要があることを意味します。

ガベージコレクターの排除:RAIIウェイ

動的メモリは、malloc、realloc、calloc、およびfreeの4つの基本操作を使用して、プログラマーによって明示的に割り当てられ、解放されました。これらの最初の2つは初期化をまったく実行せず、メモリにがらくたが含まれている可能性があります。無料を除くすべてが失敗する可能性があります。その場合、それらはnullポインターを返し、そのアクセスは未定義の動作です。最良の場合、プログラムは爆発します。最悪の場合、プログラムはしばらくの間動作しているように見え、爆発する前にガベージデータを処理します。



この方法で物事を行うことは、プログラマーであるあなたが、違反したときにプログラムを爆発させる一連の不変条件を維持する責任を単独で負うため、一種の苦痛です。変数にアクセスする前に、malloc呼び出しが必要です。変数を使用する前に、mallocが正常に返されることを確認する必要があります。実行パスには、mallocごとに1つの空き呼び出しが存在する必要があります。ゼロの場合、メモリリークが発生します。複数の場合、プログラムは爆発します。変数が解放された後は、変数へのアクセスが試行されない場合があります。これが実際にどのように見えるかの例を見てみましょう。

int main() { char *str = (char *) malloc(7); strcpy(str, 'toptal'); printf('char array = '%s' @ %u ', str, str); str = (char *) realloc(str, 11); strcat(str, '.com'); printf('char array = '%s' @ %u ', str, str); free(str); return(0); } $ make runc gcc -o c c.c ./c char * (null terminated): toptal @ 66576 char * (null terminated): toptal.com @ 66576

そのコードは、そのままでは単純ですが、すでに1つのアンチパターンと1つの疑わしい決定が含まれています。実際には、バイトカウントをリテラルとして書き出すのではなく、sizeof関数を使用する必要があります。同様に、char *配列を、2回必要な文字列のサイズに正確に割り当てます(null終了を考慮して、文字列の長さより1つ大きくします)。これは、かなりコストのかかる操作です。より洗練されたプログラムは、より大きな文字列バッファを構築し、文字列サイズを大きくすることができます。

RAIIの発明:新たな希望

控えめに言っても、そのすべての手動管理は不快でした。 80年代半ば、Bjarne Stroustrupは、彼のまったく新しい言語であるC ++の新しいパラダイムを発明しました。彼はそれをResourceAcquisitionIs Initializationと呼び、基本的な洞察は次のとおりでした。オブジェクトは、コンパイラによって適切なタイミングで自動的に呼び出されるコンストラクタとデストラクタを持つように指定できます。これにより、特定のオブジェクトのメモリを管理するためのはるかに便利な方法が提供されます。が必要であり、この手法はメモリではないリソースにも役立ちます。

これは、C ++での上記の例が、はるかにクリーンであることを意味します。

int main() { std::string str = std::string ('toptal'); std::cout << 'string object: ' << str << ' @ ' << &str << ' '; str += '.com'; std::cout << 'string object: ' << str << ' @ ' << &str << ' '; return(0); } $ g++ -o ex_1 ex_1.cpp && ./ex_1 string object: toptal @ 0x5fcaf0 string object: toptal.com @ 0x5fcaf0

手動のメモリ管理は見えません!文字列オブジェクトが作成され、オーバーロードされたメソッドが呼び出され、関数が終了すると自動的に破棄されます。残念ながら、同じ単純さが他の問題につながる可能性があります。例を詳しく見てみましょう。

vector read_lines_from_file(string &file_name) { vector lines; string line; ifstream file_handle (file_name.c_str()); while (file_handle.good() && !file_handle.eof()) { getline(file_handle, line); lines.push_back(line); } file_handle.close(); return lines; } int main(int argc, char* argv[]) { // get file name from the first argument string file_name (argv[1]); int count = read_lines_from_file(file_name).size(); cout << 'File ' << file_name << ' contains ' << count << ' lines.'; return 0; } $ make cpp && ./c++ makefile g++ -o c++ c++.cpp File makefile contains 38 lines.

それはすべてかなり簡単に思えます。ベクトル線は埋められ、返され、呼び出されます。しかし、 効率的なプログラマー パフォーマンスを気にする人は、これについて何か気になります。returnステートメントでは、値のセマンティクスが機能しているため、ベクトルは破棄される直前に新しいベクトルにコピーされます。

ドキュメントデータベースは、ドキュメントを次のような論理グループにグループ化します。

これは、最近のC ++では厳密には当てはまりません。 C ++ 11では、移動セマンティクスの概念が導入されました。この概念では、原点は有効なままですが(適切に破棄できるように)、指定されていない状態のままです。リターン呼び出しは、コンパイラがセマンティクスを移動するために最適化するための非常に簡単なケースです。これは、オリジンがそれ以上アクセスする直前に破棄されることを知っているためです。ただし、この例の目的は、80年代後半から90年代前半に人々がガベージコレクション言語を大量に発明した理由を示すことであり、当時はC ++の移動セマンティクスが利用できませんでした。

大きなデータの場合、これは高額になる可能性があります。これを最適化して、ポインタを返しましょう。構文の変更がいくつかありますが、それ以外は同じコードです。

実際には、vectorは値ハンドルです。ヒープ上のアイテムへのポインターを含む比較的小さな構造です。厳密に言えば、単にベクトルを返すことは問題ではありません。この例は、返される大きな配列の場合にうまく機能します。事前に割り当てられた配列にファイルを読み込もうとするのは無意味なので、代わりにベクトルを使用します。実用的でないほど大きなデータ構造であると偽ってください。

vector * read_lines_from_file(string &file_name) { vector * lines; string line; ifstream file_handle (file_name.c_str()); while (file_handle.good() && !file_handle.eof()) { getline(file_handle, line); lines->push_back(line); } file_handle.close(); return lines; } $ make cpp && ./c++ makefile g++ -o c++ c++.cpp Segmentation fault (core dumped)

痛い!行がポインターになったので、自動変数がアドバタイズされたとおりに機能していることがわかります。スコープが離れるとベクトルは破棄され、ポインターはスタック内の前方の位置を指します。セグメンテーション違反は単に不正なメモリへのアクセスの試みであるため、実際にはそれを予期する必要がありました。それでも、何らかの方法でファイルの行を関数から戻したいので、自然なことは、変数をスタックからヒープに移動することです。これは、新しいキーワードを使用して行われます。ファイルの1行を編集するだけで、次の行を定義できます。

vector * lines = new vector; $ make cpp && ./c++ makefile g++ -o c++ c++.cpp File makefile contains 38 lines.

残念ながら、これは完全に機能しているように見えますが、それでも欠陥があります。メモリリークです。 C ++では、ヒープへのポインターは不要になった後で手動で削除する必要があります。そうでない場合、最後のポインタがスコープから外れるとそのメモリは使用できなくなり、プロセスが終了したときにOSがメモリを管理するまで回復されません。慣用的な最新のC ++は、ここでunique_ptrを使用します。これは、目的の動作を実装します。ポインタがスコープから外れたときに指しているオブジェクトを削除します。ただし、その動作はC ++ 11まで言語の一部ではありませんでした。

この例では、これは簡単に修正できます。

vector * read_lines_from_file(string &file_name) { vector * lines = new vector; string line; ifstream file_handle (file_name.c_str()); while (file_handle.good() && !file_handle.eof()) { getline(file_handle, line); lines->push_back(line); } file_handle.close(); return lines; } int main(int argc, char* argv[]) { // get file name from the first argument string file_name (argv[1]); vector * file_lines = read_lines_from_file(file_name); int count = file_lines->size(); delete file_lines; cout << 'File ' << file_name << ' contains ' << count << ' lines.'; return 0; }

残念ながら、プログラムがおもちゃのスケールを超えて拡大するにつれて、どこでいつ正確にポインタを削除すべきかについて推論することが急速に難しくなります。関数がポインタを返すとき、あなたは今それを所有していますか?使い終わったら自分で削除する必要がありますか、それとも後ですべて一度に解放されるデータ構造に属しているのでしょうか。ある方法で間違えるとメモリリークが発生し、別の方法で間違えると、現在は無効になっているポインタを逆参照しようとするため、問題のデータ構造やおそらく他のデータ構造が破損しています。

関連: Node.jsアプリケーションでのメモリリークのデバッグ

「ガベージコレクターに、フライボーイ!」

ガベージコレクターは新しいテクノロジーではありません。それらは1959年にジョンマッカーシーによってLispのために発明されました。 1980年のSmalltalk-80で、ガベージコレクションが主流になり始めました。しかし、1990年代は、この手法の真の開花を表しています。1990年から2000年の間に、多数の言語がリリースされ、そのすべてが、Haskell、Python、Lua、Javaなどのガベージコレクションを使用していました。 JavaScript 、Ruby、OCaml、C#は最もよく知られています。

ガベージコレクションとは何ですか?つまり、手動のメモリ管理を自動化するために使用される一連の手法です。多くの場合、CやC ++などの手動メモリ管理を備えた言語のライブラリとして利用できますが、それを必要とする言語ではるかに一般的に使用されています。大きな利点は、プログラマーが単にメモリについて考える必要がないことです。それはすべて抽象化されています。たとえば、上記のファイル読み取りコードに相当するPythonは、次のとおりです。

def read_lines_from_file(file_name): lines = [] with open(file_name) as fp: for line in fp: lines.append(line) return lines if __name__ == '__main__': import sys file_name = sys.argv[1] count = len(read_lines_from_file(file_name)) print('File {} contains {} lines.'.format(file_name, count)) $ python3 python3.py makefile File makefile contains 38 lines.

行の配列は、最初に割り当てられたときに作成され、呼び出し元のスコープにコピーせずに返されます。タイミングが不確定であるため、そのスコープから外れた後、ガベージコレクターによってクリーンアップされます。興味深いことに、Pythonでは、非メモリリソースのRAIIは慣用的ではありません。許可されています-単純にfp = open(file_name)と書くこともできますwithを使用する代わりにブロックし、後でGCをクリーンアップします。ただし、推奨されるパターンは、可能な場合はコンテキストマネージャーを使用して、決定論的な時間にリリースできるようにすることです。

メモリ管理を抽象化するのは素晴らしいことですが、コストがかかります。ガベージコレクションをカウントする参照では、すべての変数の割り当てとスコープの出口は、参照を更新するためにわずかなコストがかかります。マークアンドスイープシステムでは、GCがメモリをクリーンアップしている間、予測できない間隔ですべてのプログラムの実行が停止します。これはしばしばストップザワールドイベントと呼ばれます。両方のシステムを使用するPythonのような実装には、両方のペナルティがあります。これらの問題は、パフォーマンスが重要な場合、またはリアルタイムアプリケーションが必要な場合に、ガベージコレクションされた言語の適合性を低下させます。これらのおもちゃのプログラムでも、パフォーマンスの低下が実際に見られます。

$ make cpp && time ./c++ makefile g++ -o c++ c++.cpp File makefile contains 38 lines. real 0m0.016s user 0m0.000s sys 0m0.015s $ time python3 python3.py makefile File makefile contains 38 lines. real 0m0.041s user 0m0.015s sys 0m0.015s

Pythonバージョンは、C ++バージョンのほぼ3倍のリアルタイムを必要とします。その違いのすべてがガベージコレクションに起因するわけではありませんが、それでもかなりの違いがあります。

所有権:RAIIが目覚める

それで終わりですか?すべてのプログラミング言語は、パフォーマンスとプログラミングの容易さのどちらかを選択する必要がありますか?番号!プログラミング言語の研究は続けられており、次世代の言語パラダイムの最初の実装が見られ始めています。特に興味深いのは Rustと呼ばれる言語 、これは、ダングリングポインター、nullポインターなどを不可能にしながら、Pythonのような人間工学とCのような速度を約束しますが、コンパイルされません。どうすればそれらの主張をすることができますか?

これらの印象的な主張を可能にするコアテクノロジーは、ボローチェッカーと呼ばれます。これは、コンパイル時に実行され、これらの問題を引き起こす可能性のあるコードを拒否する静的チェッカーです。ただし、その影響について深く掘り下げる前に、前提条件について説明する必要があります。

所有

C ++でのポインターの説明で、所有権の概念に触れたことを思い出してください。これは、大まかに言うと、「この変数の削除の責任者」を意味します。 Rustは、この概念を形式化して強化します。すべての変数バインディングには、バインドするリソースの所有権があり、ボローチェッカーは、リソースの全体的な所有権を持つバインディングが1つだけあることを確認します。つまり、次のスニペットは さびの本 、コンパイルされません:

let v = vec![1, 2, 3]; let v2 = v; println!('v[0] is: {}', v[0]); error: use of moved value: `v` println!('v[0] is: {}', v[0]); ^

Rustの割り当てには、デフォルトで移動セマンティクスがあります-所有権を譲渡します。型にコピーセマンティクスを与えることは可能であり、これは数値プリミティブに対してすでに行われていますが、それは珍しいことです。このため、コードの3行目以降、v2は問題のベクターを所有しており、vとしてアクセスできなくなりました。なぜこれが役立つのでしょうか。すべてのリソースに所有者が1人だけいる場合、そのリソースがスコープから外れる瞬間も1つあります。これは、コンパイル時に決定できます。つまり、Rustは、ガベージコレクターを使用したり、プログラマーが手動で何かを解放したりすることなく、スコープに基づいて決定論的にリソースを初期化および破棄し、RAIIの約束を果たすことができます。

これを参照カウントのガベージコレクションと比較してください。 RC実装では、すべてのポインターには、ポイントされたオブジェクトとそのオブジェクトへの参照の数の少なくとも2つの情報があります。そのカウントが0に達すると、オブジェクトは破棄されます。これにより、ポインターのメモリ要件が2倍になり、カウントが自動的にインクリメント、デクリメント、およびチェックされるため、使用にわずかなコストがかかります。 Rustの所有権システムは、参照がなくなるとオブジェクトが自動的に破棄されるという同じ保証を提供しますが、実行時のコストなしで破棄されます。各オブジェクトの所有権が分析され、コンパイル時に破棄呼び出しが挿入されます。

借りる

移動セマンティクスがデータを渡す唯一の方法である場合、関数の戻り値の型は非常に複雑になり、非常に高速になります。 2つのベクトルを使用して整数を生成し、後でベクトルを破棄しない関数を記述したい場合は、それらを戻り値に含める必要があります。それは技術的には可能ですが、使用するのはひどいです:

fn foo(v1: Vec, v2: Vec) -> (Vec, Vec, i32) { // do stuff with v1 and v2 // hand back ownership, and the result of our function (v1, v2, 42) } let v1 = vec![1, 2, 3]; let v2 = vec![1, 2, 3]; let (v1, v2, answer) = foo(v1, v2);

代わりに、Rustには借用の概念があります。このように同じ関数を書くことができ、それはベクトルへの参照を借用し、関数が終了したときにそれを所有者に返します。

fn foo(v1: &Vec, v2: &Vec) -> i32 { // do stuff 42 } let v1 = vec![1, 2, 3]; let v2 = vec![1, 2, 3]; let answer = foo(&v1, &v2);

v1とv2は、fn fooが戻った後、所有権を元のスコープに戻します。スコープから外れ、含まれているスコープが終了すると自動的に破棄されます。

ここで言及する価値があるのは、コンパイル時に借用チェッカーによって強制される借用に制限があり、 さびの本 非常に簡潔に置きます:

借用は、所有者の範囲を超えない範囲で継続する必要があります。次に、これら2種類の借用のいずれかを使用できますが、両方を同時に使用することはできません。

リソースへの1つ以上の参照(&T)

正確に1つの可変参照(&mut T)

これは、データの競合に対するRustの保護の重要な側面を形成するため、注目に値します。コンパイル時に特定のリソースへの複数の可変アクセスを防止することにより、どのスレッドが最初にリソースに到着したかに依存するため、結果が不確定なコードを記述できないことが保証されます。これにより、イテレータの無効化や解放後の使用などの問題が回避されます。

実用的な用語での借入チェッカー

Rustの機能のいくつかについて理解したので、前に見たのと同じファイル行カウンターを実装する方法を見てみましょう。

utf-8文字テーブル
fn read_lines_from_file(file_name: &str) -> io::Result { // variables in Rust are immutable by default. The mut keyword allows them to be mutated. let mut lines = Vec::new(); let mut buffer = String::new(); if let Ok(mut fp) = OpenOptions::new().read(true).open(file_name) // We enter this block only if the file was successfully opened. // This is one way to unwrap the Result type Rust uses instead of exceptions. // fp.read_to_string can return an Err. The try! macro passes such errors // upwards through the call stack, or continues otherwise. try!(fp.read_to_string(&mut buffer)); lines = buffer.split(' ').map( Ok(lines) } fn main() { // Get file name from the first argument. // Note that args().nth() produces an Option. To get at the actual argument, we use // the .expect() function, which panics with the given message if nth() returned None, // indicating that there weren't at least that many arguments. Contrast with C++, which // segfaults when there aren't enough arguments, or Python, which raises an IndexError. // In Rust, error cases *must* be accounted for. let file_name = env::args().nth(1).expect('This program requires at least one argument!'); if let Ok(file_lines) = read_lines_from_file(&file_name) { println!('File {} contains {} lines.', file_name, file_lines.len()); } else { // read_lines_from_file returned an error println!('Could not read file {}', file_name); } }

ソースコードですでにコメントされている項目以外にも、さまざまな変数の存続期間を調べて追跡する価値があります。 file_nameおよびfile_lines main();の終わりまで続きます。それらのデストラクタは、C ++の自動変数と同じメカニズムを使用して、追加コストなしでその時点で呼び出されます。 read_lines_from_fileを呼び出すとき、file_nameその期間中、その関数に不変に貸し出されます。 read_lines_from_file内、buffer同じように動作し、スコープから外れると破棄されます。一方、linesは持続し、mainに正常に返されます。どうして?

最初に注意することは、Rustは式ベースの言語であるため、return呼び出しは最初は1つのように見えない場合があることです。関数の最後の行で末尾のセミコロンが省略されている場合、その式が戻り値になります。 2つ目は、戻り値が特別に処理されることです。彼らは、少なくとも関数の呼び出し元と同じくらい長く生きたいと考えています。最後の注意点は、移動のセマンティクスが関係しているため、Ok(lines)を変換するためにコピーは必要ないということです。 Ok(file_lines)に挿入すると、コンパイラは変数がメモリの適切なビットを指すようにするだけです。

「最後になって初めて、RAIIの真の力に気づきます。」

手動のメモリ管理は、コンパイラの発明以来、プログラマが回避する方法を発明してきた悪夢です。 RAIIは有望なパターンでしたが、C ++では機能しませんでした。これは、いくつかの奇妙な回避策がなければ、ヒープに割り当てられたオブジェクトでは機能しなかったためです。その結果、90年代にガベージコレクションされた言語が爆発的に増加し、パフォーマンスを犠牲にしてもプログラマーの生活をより快適にするように設計されました。

しかし、それは言語デザインの最後の言葉ではありません。 Rustは、所有権と借用の新しく強力な概念を使用することにより、RAIIパターンのスコープベースとガベージコレクションのメモリセキュリティを統合することができます。他の言語では見られない安全性を保証しながら、世界を止めるためにガベージコレクターを必要とせずにすべて。これがシステムプログラミングの未来です。結局、 ' エラーを起こすのは人間ですが、コンパイラは決して忘れません。 「」


ApeeScapeエンジニアリングブログの詳細:

  • WebAssembly / Rustチュートリアル:ピッチパーフェクトなオーディオ処理
  • Javaでのメモリリークのハンティング

B2B UX –一般的な障害と達成可能なソリューション

Uxデザイン

B2B UX –一般的な障害と達成可能なソリューション
XcodeサーバーとのiOS継続的インテグレーションの説明

XcodeサーバーとのiOS継続的インテグレーションの説明

モバイル

人気の投稿
電子メール感情分析ボットを作成する方法:NLPチュートリアル。
電子メール感情分析ボットを作成する方法:NLPチュートリアル。
Redux、RxJS、およびReduxを使用したリアクティブアプリの構築-ReactNativeで観察可能
Redux、RxJS、およびReduxを使用したリアクティブアプリの構築-ReactNativeで観察可能
プレゼンテーションデザインとビジュアルストーリーテリングの芸術
プレゼンテーションデザインとビジュアルストーリーテリングの芸術
ヘルムには誰がいますか? –デザインリーダーシップの質の分析
ヘルムには誰がいますか? –デザインリーダーシップの質の分析
遺伝的アルゴリズム:自然淘汰による検索と最適化
遺伝的アルゴリズム:自然淘汰による検索と最適化
 
リモート再発明:フリーランスの仕事を見つける方法
リモート再発明:フリーランスの仕事を見つける方法
バーチャルリアリティ:仕事の未来を触媒する
バーチャルリアリティ:仕事の未来を触媒する
予測ソーシャルネットワーク分析のためのデータマイニング
予測ソーシャルネットワーク分析のためのデータマイニング
Ruby onRailsでのStripeとPayPalの支払い方法の統合
Ruby onRailsでのStripeとPayPalの支払い方法の統合
製品管理会議の包括的なリスト
製品管理会議の包括的なリスト
人気の投稿
  • SQLサーバーでのパフォーマンスの最適化
  • ロボットの制御に使用されるプログラミング言語は何ですか?
  • PythonでTwitterAPIからツイートを取得する方法
  • うなり声vsgulp vs webpack
  • Cプログラミングで何ができるか
カテゴリー
デザイナーライフ 技術 Uxデザイン 収益性と効率性 データサイエンスとデータベース プロセスとツール その他 モバイル モバイルデザイン リモートの台頭

© 2021 | 全著作権所有

apeescape2.com