54

一方では、「関数呼び出しは高価である」と聞いたり、効率に影響を与えたりします(たとえば、Nicholas ZakasのGoogleテクニカルトークで)。

ただし、一方で、ここで一般的に受け入れられているように、関数/メソッドは短くするのが最善であり、実際には1つのタスクのみを実行する必要があることは認められているようです。

私はここで何かが欠けていますか、それともこれらの2つのアドバイスは互いに逆行していませんか?禅のようなバランスを維持できる経験則はありますか?

4

5 に答える 5

48

すべての言語に適用される一般的なルールは次のとおりです。関数(メソッド、プロシージャ)を可能な限り小さくします。適切な名前を追加すると、非常に保守しやすく読みやすいコードが得られ、全体像に簡単に焦点を合わせて、興味深い詳細にドリルダウンできます。1つの巨大な方法で、常に詳細を確認し、全体像を隠すことができます。

このルールは、インライン化や実際には仮想ではないメソッドの検出などの高度な最適化を実行できる巧妙な言語とコンパイラに特に適用されるため、ダブルディスパッチは必要ありません。

JavaScriptに戻る-これはJavaScriptエンジンに大きく依存しています。場合によっては、特にタイトなループでは、実行コストを回避して、適切なエンジンがインライン関数になることを期待します。ただし、パフォーマンスの問題がない限り、小さい関数を使用することをお勧めします。読みやすさははるかに重要です。

于 2012-06-23T11:13:26.030 に答える
12

バグがなく(コードが魔法のように修正されるため)、要件が最初から凍結されている完璧な世界では、巨大な全能機能を備えた生活が可能かもしれません。

しかし、この世界では、「人月」だけでなく、高すぎることになります。Nicholas Zakasは、ソフトウェア開発者が最近直面している課題のほとんどを説明する素晴らしい記事を書きました。

移行はやや人為的に見えるかもしれませんが、私のポイントは、「1つの機能-1つのタスク」アプローチの方がはるかに保守性と柔軟性が高いということです。つまり、最終的には開発者と顧客の両方を満足させるものです。

ただし、関数呼び出しをできるだけ少なくしようとしないという意味ではありません。最優先事項ではないことを覚えておいてください。

于 2012-06-23T11:23:31.963 に答える
4

私の親指のルールは、関数が画面いっぱいの行より長い場合は、関数を小さな断片に分割するときですが、私の関数の多くは、「人為的に」分割されることなく、自然にそれよりもいくらか小さくなります。そして、私は通常、十分な空白を残しているので、画面がいっぱいでも実際には多くのコードではありません。

各関数に1つのタスクのみを実行させるようにしていますが、1つのタスクは「画面の再描画」であり、一連のサブタスクが別々の関数に実装され、それぞれが別々の関数に独自のサブタスクを持っている場合があります。

読みやすさ(したがってメンテナンスのしやすさ)について(私にとって)自然に感じるものから始めたので、テスト時に特定のコードのパフォーマンスが悪い場合を除いて、関数呼び出しが高価になることを心配しません-それから私は物事を元に戻すことを検討します-行(特に、ネストされたループで始まるループ内)。とは言っても、特定のコードがうまく機能しないことを知っていて、テストする前に書き直してしまうことがあります...

特に、舞台裏で同じ最適化を行う可能性のあるスマートコンパイラを使用する言語では、「時期尚早の最適化」を避けたいと思います。私が最初にC#を開始したとき、コードをより小さな関数に分割することは、JITコンパイラーの動作方法のために、実行時に安価になる可能性があると言われました。

1つの画面全体のルールに戻ると、JavaScriptでは(JSクロージャの動作方法のために)ネストされた関数が一般的であり、これにより、別の言語を使用している場合よりも包含関数が長くなる可能性があります。最終結果が妥協になることもあります。

于 2012-06-23T13:37:10.653 に答える
1

関数呼び出しは常に高価であり(特にサイクルの場合)、インライン化は思ったほど頻繁には行われません。

Node.js(任意のバージョン)に同梱されているV8エンジンは、広範囲にインライン化を行うことになっていますが、実際には、この機能には大きな制約があります。

次の(些細な)コードスニペットは私の主張を証明しています(Win10x64のノード4.2.1)

"use strict";

var a = function(val) {
  return val+1;
}

var b = function(val) {
  return val-1;
}

var c = function(val) {
  return val*2
}

var time = process.hrtime();

for(var i = 0; i < 100000000; i++) {
  a(b(c(100)));
}

console.log("Elapsed time function calls: %j",process.hrtime(time)[1]/1e6);

time = process.hrtime();
var tmp;
for(var i = 0; i < 100000000; i++) {
  tmp = 100*2 + 1 - 1;
}

console.log("Elapsed time NO function calls: %j",process.hrtime(time)[1]/1e6);

結果

Elapsed time function calls: 127.332373
Elapsed time NO function calls: 104.917725

+/- 20%のパフォーマンス低下

V8 JITコンパイラがこれらの関数をインライン化することを期待していましたが、実際aには、コード内の別の場所で呼び出される可能bcがあり、V8で得られる低ハンギングフルーツインライン化アプローチの候補としては適していません。

メソッドまたは関数呼び出しの乱用のために本番環境でパフォーマンスが低下するコード(Java、Php、Node.js)をたくさん見てきました。コードをMatryoshkaスタイルで作成すると、実行時のパフォーマンスは呼び出しスタックサイズに比例して低下します。概念的にきれいに見えます。

于 2015-10-27T01:04:51.250 に答える
1

すべての人に:これはより「コメント」の感触を持っています。認めた。私は「答え」のスペースを使うことにしました。ご容赦ください。

@StefanoFratini:あなたの仕事に基づいて構築するものとして私のメモをとってください。批判的であることを避けたい。

投稿のコードをさらに改善する2つの方法は次のとおりです。

  • process.hrtime()からのタプルの両方の半分を使用します。配列[秒、ナノ秒]を返します。あなたのコードはタプルのナノ秒部分(要素1)を使用していますが、秒部分(要素0)を使用していることがわかりません。
  • 単位について明確にします。

私のブラスターに合わせることができますか?ダンノ。これがStephanoのコードの開発です。欠陥があります。誰かがそれについて教えてくれても驚かないでしょう。そして、それは大丈夫でしょう。

"use strict";

var a = function(val) { return val+1; }

var b = function(val) { return val-1; }

var c = function(val) { return val*2 }

var time = process.hrtime();

var reps = 100000000

for(var i = 0; i < reps; i++) { a(b(c(100))); }

time = process.hrtime(time)
let timeWith = time[0] + time[1]/1000000000
console.log(`Elapsed time with function calls: ${ timeWith } seconds`);

time = process.hrtime();
var tmp;
for(var i = 0; i < reps; i++) { tmp = 100*2 - 1 + 1; }

time = process.hrtime(time)
let timeWithout = time[0] + time[1]/1000000000
console.log(`Elapsed time without function calls: ${ timeWithout } seconds`);

let percentWith = 100 * timeWith / timeWithout
console.log(`\nThe time with function calls is ${ percentWith } percent\n` +
    `of time without function calls.`)

console.log(`\nEach repetition with a function call used roughly ` +
        `${ timeWith / reps } seconds.` +
    `\nEach repetition without a function call used roughly ` +
        `${ timeWithout / reps } seconds.`)

それは明らかにステファノのコードの子孫です。結果はかなり異なります。

Elapsed time with function calls: 4.671479346 seconds
Elapsed time without function calls: 0.503176535 seconds

The time with function calls is 928.397693664312 percent
of time without function calls.

Each repetition with a function call used roughly 4.671479346e-8 seconds.
Each repetition without a function call used roughly 5.0317653500000005e-9 seconds.

Stephanoのように、私はWin10とNode(v6.2.0)を使用しました。

私はその議論を認めます

  • 「遠近法では、ナノ秒(10億分の1、1e-9)で、光はおよそ12インチ移動します。」
  • 「私たちはほんの数ナノ秒(47から5)について話しているので、誰がパーセンテージを気にしますか?」
  • 「一部のアルゴリズムは毎秒数十億の関数呼び出しを行うため、それらを合計します。」
  • 「私たちの開発者のほとんどはこれらのアルゴリズムを使用していないため、関数呼び出しの数を心配することは私たちのほとんどにとって逆効果です。」

私は経済的な議論に頭を悩ませます。私のコンピューターとその前のコンピューターの価格はそれぞれ400ドル(米国)未満です。ソフトウェアエンジニアが1時間あたり90ドルから130ドルのような収入を得た場合、上司に対する時間の価値は、私のような1台のコンピューターと3時間または4時間の仕事の比率になります。その環境では:

それは、必要なソフトウェアが機能しなくなったときに企業が失う1時間あたりの金額と比べてどうですか?

有料の顧客がビジネスパートナーによって作成されたシュリンクラップされたソフトウェアを一時的に使用できない場合、それは失われた善意と名声とどのように比較されますか?

他にもそのような質問がたくさんあります。省略します。

私が答えを解釈すると、読みやすさと保守性がコンピューターのパフォーマンスを支配します。私のアドバイス?それに応じて、コードの最初のバージョンを記述します。私が尊敬する多くの人々は、短い関数が役立つと言います。

コードを完成させてパフォーマンスが気に入らなくなったら、チョークポイントを見つけます。私が尊敬する多くの人々は、それらの点はあなたがそれらを期待したであろう場所では決してないと言います。あなたがそれらを知っているときに'それらを働かせなさい。

ですから、双方が正しいのです。いくつかの。

自分?私はどこかでオフになっていると思います。2セント。

于 2016-08-30T01:37:18.730 に答える