Skip to content

125. Valid Palindrome#5

Open
ryosuketc wants to merge 6 commits intomainfrom
125_valid_palindrome
Open

125. Valid Palindrome#5
ryosuketc wants to merge 6 commits intomainfrom
125_valid_palindrome

Conversation

@ryosuketc
Copy link
Owner

### step2

* https://github.com/colorbox/leetcode/pull/7/files
* `+=` しながら `checker` 文字列を作っている。Python だと文字列コピーが発生する (ただし最適化の実装による) のでやりたくない感じなんだが、C++ だとどうなの?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

C++ では文字列は mutable です。

* https://github.com/colorbox/leetcode/pull/7/files
* `+=` しながら `checker` 文字列を作っている。Python だと文字列コピーが発生する (ただし最適化の実装による) のでやりたくない感じなんだが、C++ だとどうなの?
* 上記だと `checker` を reverse している。
* step1 でのコメントも踏まえて `step2/Solution` はとりあえず書いてみた。関数では文字列コピーして返しているから冗長な感じはする。できればコピーはせずに使いたい気もする (できるのかな?無理そう)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

まず、これは named return value optimization が働いてコピーが自動的に消える可能性があります。
コピーしたくなければ、リファレンスかポインターを引数に渡します。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::string to_lower_alnum(std::string& s) {
    ...
}
bool isPalindrome(string s) {
    std::string lower_alnum_string = to_lower_alnum(s);
    ...

のような感じでしょうかね。(N)RVO の話はあまり考慮していませんでした。
https://cpprefjp.github.io/lang/cpp17/guaranteed_copy_elision.html
ちょっとフォローしきれていないので別途勉強します。

もともとここで意図していたのは、isPalindrome の引数 s の非連続 (である可能性がある) な部分文字列がlower_alnum_string であり、lower_alnum_string の中の文字 (char) はすべて s に含まれているはずなので、s のメモリを再利用して lower_alnum_string を (string_view のようなイメージで) 作れないのか、というものでした。
非連続な部分文字列なのでやはり難しい気がしました。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void to_lower_alnum(std::string& from, std::string* to);
std::string lower_alnum_string;
to_lower_alnum(s, &lower_alnum_string);

こうです。

長い文字列から数文字を消去した文字列を空間をできるだけ再利用して表現したいということですね。

Rope というデータ構造を思い出します。これは、木構造で文字列を管理して編集されていない部分木を共有します。split, concatenate が O(log n) でできます。
Piece Table はテキストエディターなどで使われるもので、変更履歴を追いかけていくものです。

一般的に連続したメモリーに対しての処理は速いので普通はコピーしたほうがいいでしょうが、テキストの編集は頻繁に行われてきたのでよく研究されています。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

概ね連続したメモリーのコピーは 1 clock で4-8バイトくらいできるのではないかと思います。
木構造をたどるとどうしてもたどるたびに数クロックかかるでしょう。

結局、切り替えることによってどれくらい実際に速くなるかを見積もって、別のライブラリーを使う必要がでてくるデメリットなどと勘案するという比較衡量をして欲しいですね。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます!

こちらでもコメントいただいている箇所ですね。先に書き込み先を用意して関数にそこに書き込んでもらう、というのが馴染みないですが、慣れていきたいと思います。

#5 (comment)

string を引数に値渡しで渡すと、コピーが発生します。stringをコピーする場合、元の文字列が格納できるサイズのメモリをヒープに確保し、文字列をコピーします。できればこのストは避けたいです。 const string& と const 参照渡ししたほうが良いと思います。
また、戻り値を string の値型で返すと、同じようにコピーが発生することが多いと思います。引数に参照型の引数を追加し、そこに出力を渡すのが良いと思います。
サイズが十分小さかったり、定数倍の速度が求められないのであれば、値渡しをしたり、値型を返しても良いかもしれません。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

概ね連続したメモリーのコピーは 1 clock で4-8バイトくらいできるのではないかと思います。
木構造をたどるとどうしてもたどるたびに数クロックかかるでしょう。

なるほど…このあたりの数字感覚がなかったのですが、どのようなロジックで推論されるものでしょうか。

追記: 後のコメントから辿って多少理解しました。32 or 64 ビット CPU の想定から 4-8 バイトと見ているんですね (ただのコピーなので機械語的にも 1 命令で、特にオーバーヘッドがない処理のはずなので)。
Ryotaro25/leetcode_first60#66 (comment)

結局、切り替えることによってどれくらい実際に速くなるかを見積もって、別のライブラリーを使う必要がでてくるデメリットなどと勘案するという比較衡量をして欲しいですね。

今回であれば 1 <= s.length <= 2 * 10^5 という制約で、ASCII で 1 char / 1 byte と仮定するなら、200KB くらいなので、丸ごとコピーすることになるとすると 4 bytes / clock と仮定して 50K clocks くらい必要。昨今の CPU なら数 GHz / sec くらいなので、1/100 秒 (10ms)とかそのくらいでしょうか。まあこのくらいならいいかな…という気がしてきます。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

先に書き込み先を用意して関数にそこに書き込んでもらう

Google のスタイルガイドもともとこうだったんですが、最近はしないようになっているみたいです。NRVO が効く前提なんでしょうか。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

数字感覚
実行時間の見積もりは、具体的な CPU によって異なる+上から抑えたいためかなり適当です。

  • 2000年頃のアーキテクチャでも浮動小数点 double は64ビットなので64ビットは一命令で動かせるでしょう。これは遅い見積もりです。
  • 変更なしでメモリーをコピーするならば SIMD 命令などではるかに速いでしょう。
  • サイズによっては L1/L2 キャッシュが効く。ないとメモリー帯域は 10 GB/s の桁。

Jeff Dean の表(2012年)は Read 1 MB sequentially from memory 250 us としていました。

なんていうか、見積もりは色々な傍証から「こんなものだよな」とやっているのでそんなにロジカルなものではないです。機械語でどうなりそうかを考えてアーキテクチャの知識と照らし合わせています。

* https://qiita.com/LdCqh1/items/92f286ceb0ab96dc3c09
* https://zenn.dev/reputeless/books/standard-cpp-for-competitive-programming/viewer/string
* > C++ 標準ライブラリは、任意の文字型 Char に対して、便利な文字列処理を提供するためのクラステンプレート std::basic_string<Char> を定義しています。それを char 型に対して特殊化(std::basic_string<char>)したものが std::string です。
* `Char` と `char` って C++ だと違うものな?(TODO)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここでの Char は一般的に Char と書けば通じるものではありません。
template <class Char> で表現するように、wchar_t などを代入するという意味(変数 x みたいな意味)です。

* `Char` と `char` って C++ だと違うものな?(TODO)
* 軽く検索してみたがよくわからん
* `union` (TODO) とかあるんだ。。
* `size_t` という型がある
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sizeof の返す型。vector size もこの型のことが多いです。
「符号なし」なので引き算をするとたまにあふれてはまります。

@@ -0,0 +1,27 @@
class Solution {
private:
std::string to_lower_alnum(std::string s) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
std::string to_lower_alnum(std::string s) {
std::string to_lower_alnum(const std::string& s) {

それかmodern C++ ではstring_viewを渡すことが多いかと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、なるほど、string_view はこういうので使えるんですね。

class Solution {
private:
std::string to_lower_alnum(std::string s) {
std::string lower_alnum_string;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

もしsのサイズが大きれば、ここは先にcapacityをreserveするのも良いかと思います。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reserve の時間見積もりはこのあたりにあります。一行を加える管理コストに足るメリットがあるかという良し悪しを評価するという方向に頭を動かしたかということが大事です。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.isflp7vsmzk2

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reserve をそもそも知らなかったのですがこれですね。
https://cpprefjp.github.io/reference/string/basic_string/reserve.html

「計算速度の見積もり」の項は何度か見ているものの、すべて身についている感じはしないので折に触れて確認していきたいと思います。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.xbcr3241jgv8

@@ -0,0 +1,27 @@
class Solution {
private:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google guideではpublicを先に宣言することがお勧めされております!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一応、プロダクションでは分割コンパイルをするというのを意識しておきましょう。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.cxz8lxsufnbn

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。このあたりを確認しました。分割コンパイルについての意識はまだ低いのでもう少し C++ に慣れたら、どういうヘッダファイルが想定されるか考えてみようと思います (production のコードでちょくちょく読んでいるので読む方の認識はあるのですが、書くほうがまだ今ひとつ)。

元の質問に戻ると、class 定義は .h に書かれてどういうクラスであるだけが書かれているので、外からのインターフェースが前で(サイズを決めるのに必要な) private 情報は後ろに来ます。ただ、普通は実体がさらに後ろに回っているので、半分は肯定ですね。


class Solution {
private:
string to_lower_alnum(string s) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string を引数に値渡しで渡すと、コピーが発生します。stringをコピーする場合、元の文字列が格納できるサイズのメモリをヒープに確保し、文字列をコピーします。できればこのストは避けたいです。 const string& と const 参照渡ししたほうが良いと思います。
また、戻り値を string の値型で返すと、同じようにコピーが発生することが多いと思います。引数に参照型の引数を追加し、そこに出力を渡すのが良いと思います。
サイズが十分小さかったり、定数倍の速度が求められないのであれば、値渡しをしたり、値型を返しても良いかもしれません。

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。こちらの oda さんのコメントと合わせて確認しました。
https://github.com/ryosuketc/leetcode_grind75/pull/5/files#r2274872702

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants