misskey icon indicating copy to clipboard operation
misskey copied to clipboard

refactor: faster for loop

Open EdamAme-x opened this issue 1 year ago • 19 comments

What

コードのリファクタリング (特にfor文) 毎回配列の長さを参照及び、計算していたのを最初に計算し、キャッシュするように変更した。

Why

毎回配列の長さを参照するのにも、計算するにもコストがかかるから。

Additional info (optional)

Checklist

EdamAme-x avatar Jan 29 '24 03:01 EdamAme-x

Codecov Report

Attention: 15 lines in your changes are missing coverage. Please review.

Comparison is base (b62d9f3) 64.21% compared to head (2b2475c) 65.72%.

Files Patch % Lines
packages/backend/src/misc/gen-identicon.ts 0.00% 4 Missing :warning:
packages/backend/src/core/FeaturedService.ts 0.00% 2 Missing :warning:
packages/backend/src/misc/prelude/array.ts 0.00% 2 Missing :warning:
...nd/src/server/api/endpoints/admin/queue/promote.ts 0.00% 2 Missing :warning:
packages/backend/src/core/HashtagService.ts 0.00% 1 Missing :warning:
...nd/src/server/api/endpoints/admin/invite/create.ts 0.00% 1 Missing :warning:
...rc/server/api/endpoints/i/notifications-grouped.ts 0.00% 1 Missing :warning:
packages/frontend/src/scripts/array.ts 0.00% 1 Missing :warning:
packages/frontend/src/scripts/i18n.ts 50.00% 1 Missing :warning:
Additional details and impacted files
@@             Coverage Diff             @@
##           develop   #13104      +/-   ##
===========================================
+ Coverage    64.21%   65.72%   +1.51%     
===========================================
  Files          976      981       +5     
  Lines       108707   113568    +4861     
  Branches      5581     5642      +61     
===========================================
+ Hits         69808    74648    +4840     
- Misses       38899    38920      +21     

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

codecov[bot] avatar Jan 29 '24 03:01 codecov[bot]

このPRによるapi.jsonの差分

差分はこちら

Get diff files from Workflow Page

github-actions[bot] avatar Jan 29 '24 03:01 github-actions[bot]

naruhodo

  • https://stackoverflow.com/questions/5752906/is-reading-the-length-property-of-an-array-really-that-expensive-an-operation
  • https://dev.to/akashkava/javascript-does-not-cache-array-length-2n65

syuilo avatar Jan 29 '24 03:01 syuilo

Lets go!

EdamAme-x avatar Jan 29 '24 03:01 EdamAme-x

naruhodo

https://zenn.dev/rabee/articles/javascript-huge-array-for-each-is-very-slow

tamaina avatar Jan 29 '24 03:01 tamaina

ドキュメント化なりlintで禁止するなりしてほしい感じがある

tamaina avatar Jan 29 '24 04:01 tamaina

こっち終わってからマージします🙏 https://github.com/misskey-dev/misskey/pull/12926

syuilo avatar Jan 29 '24 05:01 syuilo

了解です

EdamAme-x avatar Jan 29 '24 08:01 EdamAme-x

こっち終わってからマージします🙏 #12926

👀

kakkokari-gtyih avatar Jan 31 '24 02:01 kakkokari-gtyih

いちいちautogenがコンフリクトするの面倒だわね(特に生成されるものが変わる変更をしていなくても)

syuilo avatar Jan 31 '24 02:01 syuilo

常に最新の状態を維持したいので、あえて生成日時を入れてコンフリするようにしていました。 差分を見る限り、今回の対応はループ回数の取得方法変更だけです。コンフリクトが起こることがおかしいのではなく、autogen配下がコミットされていることがおかしいと思われます。

samunohito avatar Jan 31 '24 02:01 samunohito

ほむん

syuilo avatar Jan 31 '24 02:01 syuilo

V8エンジンは配列の要素数をキャッシュするのでバックエンドは変更しなくていいかも

  • https://chromium.googlesource.com/v8/v8.git/+/refs/heads/main/src/objects/js-array-inl.h
  • https://chromium.googlesource.com/v8/v8.git/+/refs/heads/main/src/objects/elements.cc

chocolate-pie avatar Jan 31 '24 02:01 chocolate-pie

常に最新の状態を維持したいので、あえて生成日時を入れてコンフリするようにしていました。 差分を見る限り、今回の対応はループ回数の取得方法変更だけです。コンフリクトが起こることがおかしいのではなく、autogen配下がコミットされていることがおかしいと思われます。

CIが引っかかったので再生成したものと思われる そしてCIがおかしくなるのはbetaリリースの際にmisskey-jsのpackage.jsonのバージョンを更新していないから(のはず)

kakkokari-gtyih avatar Jan 31 '24 02:01 kakkokari-gtyih

まあMisskeyがV8使わなくなる可能性は十分にあるわね

syuilo avatar Jan 31 '24 02:01 syuilo

まあMisskeyがV8使わなくなる可能性は十分にあるわね

(V8から移行するかは置いておいても)確実にキャッシュされる方法があるならそのように変更する分には全く問題ない気がする

kakkokari-gtyih avatar Jan 31 '24 02:01 kakkokari-gtyih

Bunが使用しているJavascriptCore.Frameworkも配列の要素数をキャッシュしてるっぽいのでやはりバックエンドを変更する必要はないかも

  • https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/runtime/JSObject.h#L196-L201
  • https://github.com/WebKit/WebKit/blob/main/Source/JavaScriptCore/runtime/JSArray.cpp#L641C10-L720

chocolate-pie avatar Jan 31 '24 05:01 chocolate-pie

firefoxで試す感じfirefoxもlengthのキャッシュの速度の差があまりない(誤差レベル)だったのですが実際にこの2つで大きな差があるフロント側の実装が何なのかが気になりました

https://hg.mozilla.org/mozilla-central/file/2cac2f68bfdcfc9012c537044bc475f076bda7b0/js/src/builtin/Array.cpp#l185 https://hg.mozilla.org/mozilla-central/file/2cac2f68bfdcfc9012c537044bc475f076bda7b0/js/src/vm/ArrayObject.h#l30 https://hg.mozilla.org/mozilla-central/file/2cac2f68bfdcfc9012c537044bc475f076bda7b0/js/src/vm/NativeObject.h#l380

anatawa12 avatar Jan 31 '24 09:01 anatawa12

横槍を入れるようで申し訳ないのですが、そもそものindexをインクリメントしつつlengthを参照するようなループを極力しないようにするという方向性でのリファクタリングは求められていないものなのでしょうか?

ざっと見た感じでも、例えばこちらの処理はArray.prototype.reduceを使うほうがよさそうですし、こちらこちらは簡単にfor...ofに置き換えることができるはずです。そもそものループや配列の処理方法を工夫したほうがより可読性も高まり、極端な場面を除き^1パフォーマンスも低下はしないはずです。

また、少し話はそれますが、インデックスを用いた配列などへのアクセスでは、手に入る値はundefinedとのunionとなります。(正確には、TSConfigでnoUncheckedIndexedAccesstrueに設定することで、undefinedとのunionとして推論されるようになります。現在のMisskeyではそのようには設定されていません。そのため、型では問題がないにも関わらず実行時にエラーになってしまう危険性がある状況です。)

どのように推論されるにしても、そもそものインデックスを用いたアクセス自体を極力取り除く方向でのリファクタリングのほうがよいように感じます。(だからといってこのPRが全面的にだめというわけではないですが……。)

パフォーマンス向上を目指すのはよいことだと思います。しかし、実際にこの変更でパフォーマンスがどの程度向上するのかは不透明です。となると、可読性や型安全性が高まるようなリファクタリングができそうな場面では、そのようなリファクタリングをするほうがよいと思います。

okayurisotto avatar Feb 18 '24 03:02 okayurisotto

ざっと見た感じでも、例えばこちらの処理はArray.prototype.reduceを使うほうがよさそうですし、こちらこちらは簡単にfor...ofに置き換えることができるはずです。そもそものループや配列の処理方法を工夫したほうがより可読性も高まり、極端な場面を除きパフォーマンスも低下はしないはずです。

手元でベンチマークを測った結果、速い順に

  • Google Chrome 123.0.6312.86 (Official Build) では: for (let i = 0; i < data.length; i++)Array.prototype.reducefor (const value of data) {
  • Safari バージョン17.4.1 (19618.1.15.11.14) では: for (let i = 0; i < data.length; i++)for (const value of data) {Array.prototype.reduce
  • Node.js v21.5.0では: for (let i = 0; i < data.length; i++)Array.prototype.reducefor (const value of data) {

となり現在の実装の方が全てのケースで速くなりました。さらにChromeで要素数が一万の配列に対して実行したところArray.prototype.reduceJSArray::kMaxFastArrayLengthを超えてないのにも関わらず八倍程度for (let i = 0; i < data.length; i++)より遅くなりました。noUncheckedIndexedAccessの件でfor ... ofなどを使うように書き換えるのは同意しますが、パフォーマンスがあまり良くないので今は書き換えるのは控えたほうが無難だと思います。

chocolate-pie avatar Mar 28 '24 10:03 chocolate-pie

これV8エンジンの最新版ではfor (let i = 0; i < data.length; i++)よりfor ... ofの方が速くなっているらしい(直そうと思ってV8の開発シェルでベンチマーク測ったら気づいた)

chocolate-pie avatar May 27 '24 12:05 chocolate-pie