スキップしてメイン コンテンツに移動

[C#]拡張メソッドでboolにNot()を生やすとどの程度パフォーマンスに影響があるのか調べてみた

はい、昨日書いた記事(if(flag == true)はありなのか?なしなのか?)で気になったので調査してみました。

ベンチマークには前々から使ってみたかったBenchmarkDotNetを使います。

同じプログラムの相対比較なのでマシンスペックには影響されないと思いますが、いちおうマシンスペック:
  • Windows 10 pro
  • core i7 6700
  • GTX970
  • RAM 16GB

比較したいものはこちら
public static class BoolExtension
{
    public static bool Not(this bool b)
    {
        return !b;
    }
}

// ~~~~

// こいつらを比較したい
if(!flag){}

// おそらくこっちは最適化されずにほんの少しパフォーマンス悪いはず
if(flag.Not()){}

はい、そうです答えはもう予測で来てるんですよね。拡張メソッドNot()は最適化されずにメソッド呼び出しのオーバーヘッドでほんの少しパフォーマンス(速度)悪いと。
それで、知りたいのはどれほどパフォーマンスに影響するか、です。

計測コード貼っておきます。
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Attributes.Jobs;
using BenchmarkDotNet.Running;

namespace BoolBenchmark
{
    public static class BoolExtension
    {
        public static bool Not(this bool b)
        {
            return !b;
        }
    }

    [ClrJob]
    public class Program
    {
        static void Main(string[] args)
        {
            BenchmarkRunner.Run();
        }

        private bool canNotCount = false;

        [Benchmark]
        public ushort Not()
        {
            ushort count = 0;
            for (ushort i = 0; i < ushort.MaxValue; i++)
            {
                if (!canNotCount)
                {
                    count++;
                }
            }
            return count;
        }

        [Benchmark]
        public ushort NotExtension()
        {
            ushort count = 0;
            for (ushort i = 0; i < ushort.MaxValue; i++)
            {
                if (canNotCount.Not())
                {
                    count++;
                }
            }
            return count;
        }
    }
}
ループ回数がushort個数回なのは許してください、uint個数回にしたら長すぎたので。

そして、結果がこちら。
あまり変わらない。。。むしろ拡張メソッドのほうが(誤差の範囲かと思いますが)微妙に早い。。。。

これはプログラムの他の箇所の最適化を疑ってみる案件かと思いますので、ILSpyで逆コンパイルして中間言語を見てみることにします。

Notメソッドforループ:
NotExtensionメソッドforループ:
むむむ。。。変な最適化はされていないようです。


いちおうBenchmarkのログを見ることにします。

// * Detailed results *
Program.Not: Clr(Runtime=Clr)
Runtime = .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0; GC = Concurrent Workstation
Mean = 43.3868 us, StdErr = 0.2119 us (0.49%); N = 17, StdDev = 0.8735 us
Min = 42.2022 us, Q1 = 42.7483 us, Median = 43.2139 us, Q3 = 44.0902 us, Max = 45.2624 us
IQR = 1.3419 us, LowerFence = 40.7354 us, UpperFence = 46.1031 us
ConfidenceInterval = [42.5362 us; 44.2374 us] (CI 99.9%), Margin = 0.8506 us (1.96% of Mean)
Skewness = 0.6, Kurtosis = 2.17


Program.NotExtension: Clr(Runtime=Clr)
Runtime = .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0; GC = Concurrent Workstation
Mean = 43.2029 us, StdErr = 0.0765 us (0.18%); N = 13, StdDev = 0.2759 us
Min = 42.7954 us, Q1 = 42.9566 us, Median = 43.2103 us, Q3 = 43.4135 us, Max = 43.7556 us
IQR = 0.4570 us, LowerFence = 42.2711 us, UpperFence = 44.0990 us
ConfidenceInterval = [42.8725 us; 43.5332 us] (CI 99.9%), Margin = 0.3304 us (0.76% of Mean)
Skewness = 0.27, Kurtosis = 1.96


Total time: 00:00:36 (36.81 sec)

// * Summary *

BenchmarkDotNet=v0.10.12, OS=Windows 10 Redstone 3 [1709, Fall Creators Update] (10.0.16299.248)
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical cores and 4 physical cores
Frequency=3328126 Hz, Resolution=300.4694 ns, Timer=TSC
  [Host] : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0
  Clr    : .NET Framework 4.6.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0

Job=Clr  Runtime=Clr  

       Method |     Mean |     Error |    StdDev |
------------- |---------:|----------:|----------:|
          Not | 43.39 us | 0.8506 us | 0.8735 us |
 NotExtension | 43.20 us | 0.3304 us | 0.2759 us |

// * Hints *
Outliers
  Program.NotExtension: Clr -> 2 outliers were removed

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 us   : 1 Microsecond (0.000001 sec)

// ***** BenchmarkRunner: End *****
// * Artifacts cleanup *

HintのところにClr -> 2 outliers were removedとありますが、よくわからない。。。。

結論

それなりの使用回数では誤差の範囲内のパフォーマンス影響なので、コアロジック以外では使っても良さそう。

このあたりのこと詳しい人いたらぜひご教授くださいm(__)m

追記:ツッコミどころとしてビルド(debug or release)どっちだよとありますが、BenchmarkDotNet先輩がdebugビルドを許してくれなかったのでreleaseビルドです。

追記の追記:VSのプロファイラーを使ってみた
プログラム変更点:ushort→uint、BenchmarkDotNetを使わずに交互に3回呼び出し

かなり回せば若干のパフォーマンス差が出るようですね。予測通りでよかったよかった。

コメント

このブログの人気の投稿

Stellaris 2.0.1 植民スパムプレイ

さっそくですが、友達との3日がかりのマルチプレイ終わりました。

銀河設定は中サイズ(600)、リング2、中盤の危機75年早く、終盤の危機100年早く、技術・伝統コスト0.5倍、難易度普通って感じでした。

そして、自分のプレイングは機械帝国植民スパム、友達は内向き牧歌でした。
最終的な国力としては機械帝国植民スパムのほうが強く、序盤から中盤は内向き牧歌のほうが強かったです。

機械帝国植民スパム はい、前回も言いましたが2.0から植民スパムゲーです。2.0.2からは前哨地維持費がかかるようになり更なる植民スパムゲーにもなりそうです。
植民スパムするからには、まぁロボットか機械帝国って感じなんですが、今回はゲーム進行が速い設定なので機械製造速度アップにガン振り帝国にしました。生産はエネルギー重視で、直轄地に鉱物惑星・研究惑星があるような感じにしました。
それでも、機械帝国は序盤の成長が遅く、鉱物を大量に消費するので、周りの帝国との不可侵条約はかかせません。直轄地6惑星(母星・エネルギー、エネルギー、研究*3、鉱物)の発展が終われば、今度はセクターの開発です。 まぁ、セクターの開発なんか物資さえ送り込めば勝手にやってくれるのでいいですが、エネルギー重視の生産なので鉱物の面倒は見てやらないとダメです。
帝国所有惑星が20ぐらいになれば、まぁ中銀河(サイズ600)なら敵なしですかね。あとは軍拡するだけです。
中盤の危機と終盤の危機 前述の通り危機はかなり早く発生するようにしたのですが、なんと中盤の危機がいっこうに発生しませんでした。発生したのは終盤の危機に立ち向かってるとき…おい()
終盤の危機は機械帝国プレイだったのでもちろんのようにコンティンジェンシー、中盤の危機は機械の反乱でした。
中盤の危機に関しては機械帝国プレイなので自分はどうでもよかったのですが、終盤の危機はがんばらないとまずかったです。コンティンジェンシーなので機械の生産-40%補正がつくので、スペシャルプロジェクトで解除(自分の研究力500ぐらいでは50か月ぐらいでした)しない限り国力が持ちません。
コンティンジェンシーは艦隊戦力が300k+100k*2の集団が4つほど湧いて100k艦隊は時間とともに増え続けるので急がないといけませんが、解除さえすればあまり余る国力で殴り続けるだけでしたね。終盤の危機の間に…

Stellaris ver2.0 Apocalypse プレイ感想

タイトルの通り大型アプデ後での友人とのマルチプレイでのプレイ感想です。

変更内容に関してはこちらのサイトで日本語訳された内容が公開されています(本当に助かりました)
Simulationian.com - 「Stellaris」開発日記#105――2.0「チェリイ」パッチノート

使用した帝国について 細かい設定は覚えていませんが(確認するのもめんどくさい)、有機生命体で狂信権威・平和主義で国是にアップデート内容の生まれた生命(Life-Seeded)を選択しています。 狂信権威について アップデートにより帝国の領域を増やすには星系ごとに影響力(1回限りに支払い)を使い前哨地を建てていく必要がありました。そこで狂信権威を選択したのですが、プレイスタイルが悪いのか、よく奴隷が不満を抱えストライキ?を起こしてしまいました。これに関しては防衛設備を地上に建て防衛軍を増やすことで対処できますが、タイルを無駄に消費している感じがすごいあります。 影響力が毎月4増えるのは領域を増やすのに好都合でしたが、Life-Seededとの相性が微妙でした(後述)。 Life-Seededについて 自分の種族がガイア型惑星にしか住めなくなるが、母星がガイア型25でスタートします。使用感想としては初期ブーストはいいかな?って感じです。ただ、アップデート後の環境ではプレイスタイルを頑張らなければ弱いかなと。その理由としては、まずアップデート後は研究コスト補正にPOP数が関係なくなり領域星系数が関係あるようになりました。つまり領域が大きければ大きいほどコストが高くなるということ。星系の資源はプレイヤーではどうしようもならない固定資源のようなものですから、それに頼って領域を大きくし過ぎると研究コストがすごいことになってしまいます。植民地を確保して賄おうにもガイア型にしか住めないのでかなり増やしにくいです。 ガイア型にしか住めないというのは、有機生命体であってロボットなら他のタイプの惑星に植民できるというのであれば、Life-Seededはロボットプレイをするしかないのかもしれないですが、これに関しては未検証です。
プレイスタイルについて 今回自分がとったプレイスタイルは要塞でガン守りキメるガチ芋戦略です。アップデートで要塞関連が変わり結構使えるものになったらしいので。 防衛プラットフォームを上限まで…

if(flag == true)はありなのか?なしなのか?

最近バズってるようなので便乗
ちなみに元ネタはこちらだとおもいます→qiita - Javaではif (flag == true)というコードを書いてはいけない

※真偽値としてのネーミングとしてflagはナシだろ~~wという話はナシでお願いします
※以下flag変数は真偽値とします

結論から言うと純粋な真偽値の場合はif(flag == true)、if(flag == false)はナシです

まず、最初に前提を整理します
if文は真偽値(true or false)で判定しなければならないJavaではbooleanKotlinではBooleanC#ではbool 話の簡素化のために整えておかないといけないこともあります JavaではBooleanのことは考えない(理由は省略)(ボクシング次第では考えていいかも)KotlinではBoolean?のことは考えないnullableな場合はtrue or false or nullなのでC#ではbool?のことは考えないnullableな場合はtrue or false or nullなので
前提を整理し終わったので、if(flag == true)はアリなのかナシなのか考えていきましょう。 おそらくif(flag == true)論争では2つのパターン(==演算子で比較するかどうか)に派閥分けされていますが、ここでは3つのパターンに分けます
1つ目のパターン:==演算子で比較する// true比較の場合 if(flag == true) // false比較の場合 if(flag == false) はい、もっともナシなパターンです(理由は後述)。

ナシと言われる理由は前提を振り返ればわかりますがif文の中では最終的に真偽値であればよいのですがflagはそもそも真偽値です。わざわざtrueと==比較して真偽値にする必要はありません。わざわざ==比較することは理論上はパフォーマンスが落ちます。

このパターンがなくならない理由は可読性がまぁまぁいいことです。
冗長であろうがパフォーマンスが理論上悪かろうが可読性があれば正義です(そういう考えもできる)。

自分も昔はこのパターンな書き方を多用していましたが後述の理由によって今はしません(少なくとも意識してはしてないはず)。

2つ目のパターン:否定演算子!を使う// true比較の…