Skip to content

Conversation

@Ryotaro25
Copy link
Owner

問題へのリンク
ZigZag Conversion
問題文(プレミアムの場合)

備考

次に解く問題の予告
Contains Duplicate

フォルダ構成
LeetCodeの問題ごとにフォルダを作成します。
フォルダ内は、step1.cpp、step2.cpp、step2_1.cpp、step2_2.cpp、step3.cppとmemo.mdとなります。

memo.md内に各ステップで感じたことを追記します。

https://www.reddit.com/r/cpp_questions/comments/wxiyg1/most_efficient_way_to_concatenate_strings/

こちらの中でも色々議論されている
reserveを使って予め必要なメモリを確保しておけば先確保を減らすことができる。
Copy link

Choose a reason for hiding this comment

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

こういう話、速くしたいということが目的ならば、どれくらい速くなるかを「秒」なりで具体的に概算してください。
https://discord.com/channels/1084280443945353267/1200089668901937312/1202604551815954463

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
レビューありがとうございます。
下記のようにメモに追加しました。
今回stringの最長は1000文字。numRowsが1000の場合、一文字ずつ繋げる必要がある。 下記の記事によると、環境にもよるがだいたい初期は32文字まで確保されている。 https://stackoverflow.com/questions/53216377/how-much-memory-is-allocated-to-an-uninitialized-stdstring-variable 1000 / 32 ≒ 31なので、31回リアロケーションが発生する。 あらかじめreserveで確保しておくと、1度ですむ。

Copy link

Choose a reason for hiding this comment

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

@Ryotaro25 それは秒ではないです。なぜ、秒でといっているかというと、メリットがある程度定量化できないと、その選択を取ったときのデメリットと比較ができないからです。

また、仕様上、計算量は定数時間で償却できるようにする必要があるので、普通は doubling するのが一般的です。
capacity でサイズは調べられます。

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

それは秒ではないです。なぜ、秒でといっているかというと、メリットがある程度定量化できないと、その選択を取ったときのデメリットと比較ができないからです。

秒以外の何かしらの数値でもいいと理解しておりました🙇‍♂️
capcityを用いてリアロケーションの確認を行い、#include を使って速度は計測してみました。
実行環境はMac OSです。

sは1000文字にしてリアロケーションがどのように発生するのか確認
capcityの変化 : 22 47 95 191 383 767 1535
=>リアロケーションに対する理解に誤りがございました。下記の通りでした。

仕様上、計算量は定数時間で償却できるようにする必要があるので、普通は doubling するのが一般的です。

#include を用いて実行速度を計測する。
https://www.rk-k.com/archives/6973
・sは1000文字、numRows = 1000
reserve無し
処理時間: 0.000161757 秒
reserveあり
処理時間: 0.000206844 秒

・sは1000文字、numRows = 1000
reserve無し
処理時間: 3.6マイクロ 秒
reserveあり
処理時間: 3.6マイクロ 秒

reserveを使うことで、リアロケーションは発生しなくなったが
1000文字では、実行速度に差が見られなかった。なのでリアロケーションの発生しないようにreserveを使うのがいいかなと思いました。

Copy link

Choose a reason for hiding this comment

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

すみません、これはそれぞれどういうコードを走らせた時の何の数字ですか。

Copy link

Choose a reason for hiding this comment

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

さて、capacity の変化を見ると、合計1500文字くらい reallocation されていますね。
これが、概ね 50 ns というのは直感にあいますか。

機械語の知識の問題になりますが、CPU の周波数が 3GHz として、150クロックくらい。1クロックで8文字ずつ(64ビット)コピーされるとすると、1200文字となっておおむね辻褄が合います。

要するに、reserve を足すと、概ね3マイクロ秒で動くコードを 50 ナノ秒コードを改善したということです。このためにこの一行足す価値ありますか。

Copy link

Choose a reason for hiding this comment

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

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
ありがとうございます。

こういうのは統計誤差が大きいので、何回も呼んで、平均を取ります。

覚えておきます🙇‍♂️
参照先も確認しました。まさに自分は相対的な評価で考えておりました。

さて、capacity の変化を見ると、合計1500文字くらい reallocation されていますね。
これが、概ね 50 ns というのは直感にあいますか。
機械語の知識の問題になりますが、CPU の周波数が 3GHz として、150クロックくらい。1クロックで8文字ずつ(64ビット)コピーされるとすると、1200文字となっておおむね辻褄が合います。

直感に合うかどうか答えられなかったです。周波数やクロックという単語を認識しておりますが数値周りの理解ができていないので調べながら手元で計算しました。

1GHzだと、1sあたりに10^9(10億)回クロック信号が発信される。
3GHzだと、1sあたりに3 * 10^9 回クロック信号が発信される。
1s = 10^(-9) nsであり1nsあたりだと3クロックとなる。50ns(実験から)は、150クロックとなる。
64ビットCPUが1命令あたりに8バイト処理できると想定すると、150クロックで1200文字 (C++のChar1文字は1バイト)処理することができる。
・店頭に並ぶ多くのCPUのクロック周波数は、基本的に3.0 GHz台
https://chimolog.co/bto-cpu-clock/
・一般的なPCは64ビットCPU(x86-64)
https://ja.wikipedia.org/wiki/X64

要するに、reserve を足すと、概ね3マイクロ秒で動くコードを 50 ナノ秒コードを改善したということです。このためにこの一行足す価値ありますか。

レスポンスに対する要件を最重要とするならば入れてもいいと思いました。ただ入れることで今後、入力が1000文字以上になってくると固定値で管理しているので拡張性はないと思いました。

Copy link

Choose a reason for hiding this comment

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

そうですね。これが1ヶ月かかるプログラムのタイトなループの部分ならば半日速くなる話なので考えますね。

ただ、普通 50 ns 速くする方法というのは大量にあるので、それを全部採用していられないので、扱いやすさを優先します。

最低限、「パレート最適」、つまり、何かを改善しようとすると、何かが悪くなる、くらいにはよいコードを書きたいです。その中では比較的、コードの複雑さ(code complexity) が優先される事が多いです。
読みやすく、問題があったときにデバッグしやすく、修正しやすいのか、つまり、すべてのコードは未来においてマイナスを生み出しうるが、それは小さいのか、ということです。

https://discord.com/channels/1084280443945353267/1262688866326941718/1346868225446641678

Copy link

Choose a reason for hiding this comment

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

あ、もちろんどれを選ぶかは状況によります。

たとえば、標準ライブラリーだとどういう使われ方がするか分からない中で可能な限りチューニングします。Python の標準ライブラリーは一部 C で実装されていたりしますね。

step2_1.cpp
step2.cppのvector<vector<char>>からvector<string>へ
Yoshiki-Iwasaさんの回答を参照
何度か調べた気がするが効率のいいstringの繋げ方を調べてみる
Copy link

Choose a reason for hiding this comment

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

Java や Python など文字列が immutable な言語では重要な話ですが、C++ では、mutable で後ろに文字をつける分には大きな問題になりません。

前後に付けたり分割したりなどする必要があるならば、Rope というデータ構造などを使えばいいですが、そこまでする必要があることは少ないです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

他言語のことをあまり意識できておりませんでした。
StringBuilderやjoinをレビュー時に見かけるのはこういった理由ですね。

if (row == numRows - 1) {
is_downforward = false;
}
if (row == 0) {

Choose a reason for hiding this comment

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

else ifとした方が僕は読みやすいと思いました

Copy link
Owner Author

Choose a reason for hiding this comment

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

@philip82148
ありがとうございます。確かに対称性があるのでこちらの方が読みやすいですね。
step4に追加しました。

}
}

string converted = "";

Choose a reason for hiding this comment

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

string conveted;で十分かつより効率的です(コンパイラの最適化でどっちも同じになりそうですが)。

Copy link
Owner Author

Choose a reason for hiding this comment

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

この部分ですが
オブジェクトが初期化されるルールがあるのは知っているのですが、型毎に異なりルールは複雑であるためイブジェクトを使う前には初期化しよう。というeffective C++に書かれていたことに従いました🙇‍♂️
stringなど基本的なものに関しては覚えた方が良さそうですんが。。。

Copy link

@philip82148 philip82148 Apr 7, 2025

Choose a reason for hiding this comment

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

C++ではオブジェクトは宣言と同時に初期化されます(コンストラクタが呼び出されます)よ
オブジェクトじゃない組み込み型等(char、int、bool等)は初期化しないと値が不定になります
多分effective C++に書かれていたのはそっちじゃないでしょうか?

C++のオブジェクトはいろんなコンストラクタ呼び出しの仕方があって、

// デフォルトコンストラクタ呼び出し
string str;

// 明示的に呼び出す
string str(5, 'a');

// 変換コンストラクタ
string str = "aiueo";
// これは以下と同じ
string str("aiueo");

// 初期化子リスト(initializer lists)
string str({'a', 'i', 'u', 'e', 'o'});

// 一様初期化
string str{5, 'a'};
string str{"aiueo"};
string str{'a', 'i', 'u', 'e', 'o'};

// なお変換コンストラクタは右辺を第一引数にしてコンストラクタ呼び出ししているだけなので、
// (コンストラクタ定義にexplicitというキーワードが付いていない限り)他でもできます
string str = {'a', 'i', 'u', 'e', 'o'};

他にもコピーコンストラクタやムーブコンストラクタ等があります。
どのコンストラクタを呼び出しているかは、毎回調べるのが良いと思いますよ
https://cpprefjp.github.io/reference/string/basic_string/op_constructor.html

Copy link
Owner Author

Choose a reason for hiding this comment

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

@philip82148 ありがとうございます。effective C++の19Pに組み込み型は初期化されない、vectorはいつでも保証されるのでこういった状況に対する良い方法はオブジェクトは使う前に必ず初期化する。と書かれておりました。

私は、この部分を組み込み型であってもなくてもオブジェクトは使う前に初期化しておけば安全と理解しておりました。

これは誤りかもしれませんので後ほどこの章を読み返してみます。

Copy link
Owner Author

Choose a reason for hiding this comment

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

@philip82148

どのコンストラクタを呼び出しているかは、毎回調べるのが良いと思いますよ

ここはあまり意識したことが無かったです。ありがとう御座います🙇

if (numRows == 1) {
return s;
}
vector<string> string_per_row(numRows);

Choose a reason for hiding this comment

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

僕はシンプルにrowsとします

}
vector<string> string_per_row(numRows);

bool is_downforward = true;

Choose a reason for hiding this comment

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

int direction = 1; // or -1
row += direction;

というパターンもありです。

for (const auto& str : string_per_row) {
converted += str;
}
return converted;

Choose a reason for hiding this comment

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

return reduce(string_per_row.begin(), string_per_row.end());

でもいい(はず)です。

Copy link
Owner Author

Choose a reason for hiding this comment

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

@philip82148
reduceを知らなかったのでありがたいです。
https://en.cppreference.com/w/cpp/algorithm/reduce

step4で使ってみました。
内部的には毎回コピーが発生してそうなことは気になりました。

Copy link

@philip82148 philip82148 Apr 7, 2025

Choose a reason for hiding this comment

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

それは確かにですね。そうなると

return accumulate(string_per_row.begin(), string_per_row.end(), string());

ともできます。
この場合コピーは発生していなそうです。(おそらくC++20以降のみ)

参考:
https://timsong-cpp.github.io/cppwp/n4861/accumulate#2
https://timsong-cpp.github.io/cppwp/n4861/string.op.plus#2

@Ryotaro25
Copy link
Owner Author

見積もることについて。

100人で会議をします、弁当の予算はいくら必要ですか、と聞かれて「人数に比例する額です」以上の答えが出てこなかったらやばいやつでしょ。「比例することは分かったから、それでいったいいくらを予算だと思うの」という質問です。これに対しては、。一人1000円でも3000円でも別にいくらでも構わないのですが、具体的な値が出てくることが大事です。

https://discord.com/channels/1084280443945353267/1200089668901937312/1202604551815954463

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants