翻訳者から

これは、私が翻訳しているHerman RadtkeのRust and String storage and memoryシリーズの最後の記事です。 それは私にとって最も有用であるように思われ、最初はそれから翻訳を始めたいと思っていましたが、その後、シリーズの残りの記事もコンテキストを作成し、この記事が失われない言語のよりシンプルだが非常に重要な瞬間に導入するために必要であるように思われましたユーティリティ。
引数として
Stringまたは&str (
英語 )
を取る関数を作成する方法を学び
ました 。 ここで、
Stringまたは
&strを返す関数を作成する方法を示し
&str 。 また、なぜこれが必要なのかも議論したい。
まず、指定された文字列からすべてのスペースを削除する関数を作成しましょう。 関数は次のようになります。
fn remove_spaces(input: &str) -> String { let mut buf = String::with_capacity(input.len()); for c in input.chars() { if c != ' ' { buf.push(c); } } buf }
この関数は、文字列バッファーにメモリを割り当て、
input文字列のすべての文字を反復処理し、すべての非空白文字を
bufバッファーに追加します。 質問は次のとおりです。入力に単一のスペースがない場合はどうなりますか? その場合、
input値は
bufとまったく同じになります。 この場合、
bufをまったく作成しないほうが効率的です。 代わりに、与えられた
input関数のユーザーに返したいだけです。
inputタイプは
&strですが、この関数は
String返します。
inputタイプを
String変更できます。
fn remove_spaces(input: String) -> String { ... }
しかし、2つの問題があります。 まず、
inputが
Stringになった場合、関数のユーザーは
input の所有権を関数に
移動する必要があるため、将来同じデータを処理できなくなります。 本当に必要な場合にのみ、
inputを取得する必要があります。 次に、入力はすでに
&strである可能性があり、ユーザーに文字列を
Stringに変換するように強制し、
bufメモリを割り当てないようにする試みを無効にします。
レコードの複製
実際、スペースがない場合は入力文字列(
&str )を返し、スペースがある場合は新しい文字列(
String )を返し、それらを削除する必要があります。 これが、コピーオンライトタイプ(クローン-オン-ライト)の
牛が助けになる場所です。
Cowタイプでは、変数を所有している(
Owned )か、単に借りた(
Borrowed )かを無視できます。 この例では、
&strは既存の文字列への参照であるため、これは
借用データになります。 文字列にスペースがある場合、新しい
Stringメモリを割り当てる必要があります。
buf変数
はこの文字列を
所有しています。 通常、
buf所有権を
移動し、ユーザーに返します。
Cowを使用する場合、
buf の所有権を
Cowタイプに
移動してから、すでに所有権を返したいと考えています。
use std::borrow::Cow; fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> { if input.contains(' ') { let mut buf = String::with_capacity(input.len()); for c in input.chars() { if c != ' ' { buf.push(c); } } return Cow::Owned(buf); } return Cow::Borrowed(input); }
この関数は、元の
input引数に少なくとも1つのスペースが含まれているかどうかをチェックしてから、新しいバッファーにメモリを割り当てます。
inputにスペースが含まれていない場合、そのまま返されます。 メモリ処理を最適化するために
、実行時に少し
複雑になります 。
Cowタイプの寿命は
&strと同じです。 前述したように、コンパイラは
&strリンクの使用を監視して、いつメモリを解放しても安全かを判断する必要があります(または、型が
Drop実装している場合はデストラクタメソッドを呼び出します)。
Cowの
Derefは
Deref実装しているため、結果に新しいバッファが割り当てられているかどうかを知らなくても、これらのデータを変更しないメソッドを呼び出すことができることです。 例:
let s = remove_spaces("Herman Radtke"); println!(" : {}", s.len());
sを変更する必要がある場合は、
into_owned()メソッドを使用して
所有変数に変換できます。
Cowに借用データが含まれる場合(
Borrowed選択されている場合)、メモリが割り当てられます。 このアプローチにより、変数への書き込み(または変更)が本当に必要な場合にのみ、遅延してクローンを作成(つまり、メモリを割り当て)することができます。
変更可能な
Cow::Borrowed例
Cow::Borrowed :
let s = remove_spaces("Herman");
変更可能な
Cow::Owned例
Cow::Owned :
let s = remove_spaces("Herman Radtke");
Cowアイデアは次のとおりです。
- できるだけ長くメモリの割り当てを延期する。 最良の場合、新しいメモリを割り当てることはありません。
remove_spaces関数のユーザーがメモリ割り当てを気にしないようにするため。 とにかくCow使用は同じです(新しいメモリが割り当てられるかどうか)。
Into特性を使用する
Intoトレイトを使用して
&strを
Stringに変換することについて話していました。 同様に、これを使用して
&strまたは
Stringを目的の
Cowオプションに変換できます。
.into()を呼び出すと、コンパイラは正しい変換オプションを自動的に選択します。
.into()を使用してもコードが遅くなることはありません。これは、
Cow::Ownedまたは
Cow::Borrowedを明示的に指定することをなくすための方法にすぎません。
fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> { if input.contains(' ') { let mut buf = String::with_capacity(input.len()); let v: Vec<char> = input.chars().collect(); for c in v { if c != ' ' { buf.push(c); } } return buf.into(); } return input.into(); }
最後に、イテレータを少し使用して例を簡単にできます。
fn remove_spaces<'a>(input: &'a str) -> Cow<'a, str> { if input.contains(' ') { input .chars() .filter(|&x| x != ' ') .collect::<std::string::String>() .into() } else { input.into() } }
牛の実際の使用
スペースを削除する私の例は少々難易度が高いように見えますが、実際のコードではこの戦略もアプリケーションを見つけます。 Rustカーネルに
は、無効なバイトの組み合わせを
なくしてバイトをUTF-8ストリングに変換する関数と、
行末をCRLFからLFに変換する関数があります。 これらの関数の両方について、最適なケースで
&strを返すことができる場合と、
Stringメモリ割り当てを必要とする最適でないケースがあります。 私の頭に浮かぶ他の例は、文字列を有効なXML / HTMLにコーディングするか、SQLクエリで特殊文字を正しくエスケープすることです。 多くの場合、入力データは既に正しくエンコードまたはシールドされているため、単純に入力文字列をそのまま返す方が適切です。 データを変更する必要がある場合は、文字列バッファーにメモリを割り当てて、既に返す必要があります。
String :: with_capacity()を使用する理由
効率的なメモリ管理について話している間、文字列バッファーの作成時に
String::new()代わりに
String::new() String::with_capacity()を使用したことに注意してください。
String::with_capacity() String::new()代わりに
String::new()使用できますが、バッファーに新しい文字を追加するときに再割り当てするのではなく、バッファーに必要なすべてのメモリを一度に割り当てる方がはるかに効率的です。
Stringは、実際にはUTF-8コードポイントからの
Vecベクトルです。
String::new()呼び出されると、Rustは長さゼロのベクトルを作成します。 たとえば、
input.push('a')を使用して文字列バッファーに文字
aを配置すると、Rustはベクトルの容量を増やす必要があります。 これを行うには、2バイトのメモリを割り当てます。 さらにバッファーに文字を配置し、割り当てられたメモリサイズを超えると、Rustは行のサイズを2倍にし、メモリを再割り当てします。 彼はベクトルが超過するたびに容量を増やし続けます。 割り当てられた容量のシーケンスは次のとおりです:
0, 2, 4, 8, 16, 32, …, 2^n 、ここでnは割り当てられたメモリを超えたことをRustが検出した回数です。 メモリの再割り当てが非常に遅い(訂正:kmc_v3
は 、思ったほど遅くないかもしれないと
説明した)。 Rustはカーネルに新しいメモリを割り当てるように要求するだけでなく、ベクトルの内容を古いメモリから新しいメモリにコピーする必要もあります。
Vec :: pushのソースコードを見て、自分でベクトルのサイズを変更するためのロジックを確認してください。
kmc_v3からのメモリ割り当ての更新すべてがそれほど悪くないかもしれません:
- 適切なアロケーターは、OSに大きなチャンクでメモリを要求し、それをユーザーに提供します。
- まともなマルチスレッドメモリアロケーターも各スレッドのキャッシュをサポートしているため、常にアクセスを同期する必要はありません。
- 非常に頻繁に、割り当てられたメモリを所定の場所に増やすことができます。そのような場合、データのコピーは行われません。 100バイトしか割り当てられていない場合もありますが、次の1000バイトが空いている場合は、アロケータがそれらを単に与えます。
- コピーの場合でも、
memcpyバイトコピーmemcpy 、完全に予測可能な方法でメモリにアクセスします。 したがって、これはおそらくメモリからメモリにデータを移動する最も効率的な方法です。 libcシステムライブラリには通常、特定のマイクロアーキテクチャ用に最適化されたmemcpyが含まれています。 - MMUを再構成することで、割り当てられた大きなメモリチャンクを「移動」することもできます。つまり、1ページのデータをコピーするだけで済みます。 ただし、通常、ページテーブルの変更には大きな固定コストがかかるため、この方法は非常に大きなベクトルにのみ適しています。 Rustの
jemallocがそのような最適化を行うかどうかはjemallocません。
C ++での
std::vectorサイズ変更は、要素ごとにmoveコンストラクターを個別に呼び出す必要があるため非常に遅くなり、例外をスローする可能性があります。
一般に、新しいメモリは、必要なときにのみ、必要なだけ割り当てるようにします。
remove_spaces("Herman Radtke")などの短い行の場合、メモリ割り当てのオーバーヘッドは大きな役割を果たしません。 しかし、サイト上のすべてのJavaScriptファイル内のすべてのスペースを削除したい場合はどうすればよいですか? バッファにメモリを再割り当てするオーバーヘッドははるかに大きくなります。 データをベクター(
Stringまたはその他)に配置する場合、ベクターの作成時に必要なメモリーのサイズを示すことは非常に便利です。 最良の場合、ベクトルの容量を正確に設定できるように、必要な長さを事前に知っています。
Vec コードに対するコメントは、同じことについて警告しています。
他に読むものは何ですか?