翻訳者から
これは、私が翻訳している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"); // s Cow::Borrowed let len = s.len(); // Deref let owned: String = s.into_owned(); // String
変更可能な
Cow::Owned
例
Cow::Owned
:
let s = remove_spaces("Herman Radtke"); // s Cow::Owned let len = s.len(); // Deref let owned: String = s.into_owned(); // , String
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からのメモリ割り当ての更新
すべてがそれほど悪くないかもしれません:
C ++での
サイズ変更は、要素ごとにmoveコンストラクターを個別に呼び出す必要があるため非常に遅くなり、例外をスローする可能性があります。
- 適切なアロケーターは、OSに大きなチャンクでメモリを要求し、それをユーザーに提供します。
- まともなマルチスレッドメモリアロケーターも各スレッドのキャッシュをサポートしているため、常にアクセスを同期する必要はありません。
- 非常に頻繁に、割り当てられたメモリを所定の場所に増やすことができます。そのような場合、データのコピーは行われません。 100バイトしか割り当てられていない場合もありますが、次の1000バイトが空いている場合は、アロケータがそれらを単に与えます。
- コピーの場合でも、
memcpy
バイトコピーmemcpy
、完全に予測可能な方法でメモリにアクセスします。 したがって、これはおそらくメモリからメモリにデータを移動する最も効率的な方法です。 libcシステムライブラリには通常、特定のマイクロアーキテクチャ用に最適化されたmemcpy
が含まれています。 - MMUを再構成することで、割り当てられた大きなメモリチャンクを「移動」することもできます。つまり、1ページのデータをコピーするだけで済みます。 ただし、通常、ページテーブルの変更には大きな固定コストがかかるため、この方法は非常に大きなベクトルにのみ適しています。 Rustの
jemalloc
がそのような最適化を行うかどうかはjemalloc
ません。
C ++での
std::vector
サイズ変更は、要素ごとにmoveコンストラクターを個別に呼び出す必要があるため非常に遅くなり、例外をスローする可能性があります。
一般に、新しいメモリは、必要なときにのみ、必要なだけ割り当てるようにします。
remove_spaces("Herman Radtke")
などの短い行の場合、メモリ割り当てのオーバーヘッドは大きな役割を果たしません。 しかし、サイト上のすべてのJavaScriptファイル内のすべてのスペースを削除したい場合はどうすればよいですか? バッファにメモリを再割り当てするオーバーヘッドははるかに大きくなります。 データをベクター(
String
またはその他)に配置する場合、ベクターの作成時に必要なメモリーのサイズを示すことは非常に便利です。 最良の場合、ベクトルの容量を正確に設定できるように、必要な長さを事前に知っています。
Vec
コードに対するコメントは、同じことについて警告しています。