RustでStringまたは&strを返す関数を作成する

翻訳者から


これは、私が翻訳している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つの問題があります。 まず、 inputStringになった場合、関数のユーザーは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実装している場合はデストラクタメソッドを呼び出します)。

CowDerefDeref実装しているため、結果に新しいバッファが割り当てられているかどうかを知らなくても、これらのデータを変更しないメソッドを呼び出すことができることです。 例:

 let s = remove_spaces("Herman Radtke"); println!(" : {}", s.len()); 

sを変更する必要がある場合は、 into_owned()メソッドを使用して所有変数に変換できます。 Cowに借用データが含まれる場合( Borrowed選択されている場合)、メモリが割り当てられます。 このアプローチにより、変数への書き込み(または変更)が本当に必要な場合にのみ、遅延してクローンを作成(つまり、メモリを割り当て)することができます。

変更可能なCow::BorrowedCow::Borrowed

 let s = remove_spaces("Herman"); // s   Cow::Borrowed let len = s.len(); //         Deref let owned: String = s.into_owned(); //      String 

変更可能なCow::OwnedCow::Owned

 let s = remove_spaces("Herman Radtke"); // s   Cow::Owned let len = s.len(); //         Deref let owned: String = s.into_owned(); //    ,      String 

Cowアイデアは次のとおりです。


Into特性を使用する


Intoトレイトを使用して&strStringに変換することについて話していました。 同様に、これを使用して&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 コードに対するコメントは、同じことについて警告しています。

他に読むものは何ですか?


Source: https://habr.com/ru/post/J274565/


All Articles