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

[C#]stackallocでstructをスタックに確保してパフォーマンスを上げよう戦略


略してstackalloc戦略です。

前回の記事でin引数戦略(最終的にはref戦略な感じになりましたけど)を提示してみましたが、in引数戦略では使える幅が狭いです。

なぜかというと、配列が使えないから。

.NETでは配列はヒープに確保されるので、原理上、配列の値をローカル変数に持ってくると値のコピーが発生してしまいます。
それではin引数戦略はほとんどの場合で使い物になりません。

そこでstackalloc戦略です。
C# 7.2で安全なstackallocであればunsafeコンテキスト以外で使用できるようになりました。安全というのはSpan<T>と併用している場合です。
(Span<T>は.NET Core 2.1以外ではSystem.Memoryパッケージが必要とされます)

stackallocを用いてスタック上に配列のようなもの(Span<T>)を確保することで、in引数戦略の効力を高めようという感じです。

肝心のベンチマーク

今回もBenchmarkDotNetを使用しています。
今回は.NET Core 2.0と2.1のパフォーマンス差も確認したいので、csprojは以下のような感じの記述を追加しています。
    <TargetFrameworks>netcoreapp2.0;netcoreapp2.1;net47</TargetFrameworks>
    <PlatformTarget>AnyCPU</PlatformTarget>
また、CoreJobもそのままでは使えないので、新しく属性を定義してやります。
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)]
    public class Core20JobAttribute : Attribute, IConfigSource
    {
        public Core20JobAttribute()
        {
            Config = ManualConfig.CreateEmpty().With(Job.Core.With(CsProjCoreToolchain.NetCoreApp20));
        }

        public IConfig Config { get; }
    }

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Assembly)]
    public class Core21JobAttribute : Attribute, IConfigSource
    {
        public Core21JobAttribute()
        {
            Config = ManualConfig.CreateEmpty().With(Job.Core.With(CsProjCoreToolchain.NetCoreApp21));
        }

        public IConfig Config { get; }
    }

そしてベンチマーク対象はこんな感じのメソッドです。
        [Benchmark]
        public int NormalIndexOf()
        {
            int[] ar = new[] { 1, 4, 7, 8, 0, 1 };
            for (int i = 0; i < ar.Length; i++)
            {
                if (ar[i] == 0)
                {
                    return i;
                }
            }
            return -1;
        }

        [Benchmark]
        public int SpanIndexOf()
        {
            Span ar = stackalloc[] { 1, 4, 7, 8, 0, 1 };
            for (int i = 0; i < ar.Length; i++)
            {
                if (ar[i] == 0)
                {
                    return i;
                }
            }
            return -1;
        }
そして、ついでにin引数戦略っぽいベンチマークもしましょう。
        public readonly struct Size8
        {
            public readonly int A, B;

            public Size8(int a, int b)
            {
                A = a;
                B = b;
            }
        }

        [Benchmark]
        public Size8 NormalMax8()
        {
            Span ar = new[] {
                new Size8(1, 2),
                new Size8(2, 2),
                new Size8(3, 3),
                new Size8(1, 1)
            };

            Size8 max = ar[0];
            for (int i = 1; i < ar.Length; i++)
            {
                Size8 x = ar[i];
                if (max.A + max.B < x.A + x.B)
                {
                    max = x;
                }
            }
            return max;
        }

        [Benchmark]
        public Size8 SpanMax8()
        {
            Span ar = stackalloc[] {
                new Size8(1, 2),
                new Size8(2, 2),
                new Size8(3, 3),
                new Size8(1, 1)
            };

            ref Size8 max = ref ar[0];
            for (int i = 1; i < ar.Length; i++)
            {
                ref Size8 x = ref ar[i];
                if (max.A + max.B < x.A + x.B)
                {
                    max = ref x;
                }
            }
            return max;
        }

そしてこれのSize16版もしました。 ソースコード全体はgistに上げています。

そしてベンチマーク結果ですが、.NET Core 2.1つょぃという結果に。

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.492 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-6700 CPU 3.40GHz (Skylake), 1 CPU, 8 logical and 4 physical cores
Frequency=3328123 Hz, Resolution=300.4697 ns, Timer=TSC
.NET Core SDK=2.1.300
  [Host]     : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  Job-ANIBAB : .NET Core 2.0.7 (CoreCLR 4.6.26328.01, CoreFX 4.6.26403.03), 64bit RyuJIT
  Job-TCMRRU : .NET Core 2.1.0 (CoreCLR 4.6.26515.07, CoreFX 4.6.26515.06), 64bit RyuJIT
  Clr        : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2671.0

.NET Core 2.1ではstackalloc/Spanが早い結果が出ましたが、その他では致命的に遅いという結果に。。。

これは、.NET Core 2.1以外ではSpan<T>に対する特殊対応が入っていないからと推測できます。

.NET Core 2.1ならstackalloc戦略は有効か?

一概にもそういうわけではないです。そもそもスタック領域はあまり容量のない領域ですので、それなりに大きいサイズのstackallocはしないほうがいいかと思います。
(いろいろ調べてみるとスタック領域は標準で1MBらしいです)

まとめ

  • .NET Core 2.1は強かった
  • .NET Core 2.1ならstackalloc戦略は使えそう
  • しかし、現状は.NET Core 2.1以外の環境を考えると使いどころに悩む
  • 容量用法を正しく守ってin引数戦略・stackalloc戦略を採りましょうね
今回は結構ガバガバなベンチマークを取っていますが、もっといいベンチマークを取ってみたよ!などがあればご一報くださいm(__)m

コメント

このブログの人気の投稿

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比較の…