20200118のC#に関する記事は12件です。

AllocHGlobal と AllocCoTaskMem で迷ったら、やっぱり AllocCoTaskMem を使え!

はじめに

.NET のアンマネージドメモリの処理速度 について調べていたところ、以下の記事を見つけました。

AllocHGlobalとAllocCoTaskMem どちらを使うべきか?

私はこの記事に出会うまで AllocCoTaskMem の存在を知らなかったので、何も考えず AllocHGlobal を使ってきたのですが、記事を読んだところ以下の結論になっていました。

「パフォーマンス的に多少の前後はあるが、AllocHGlobalとAllocCoTaskMemに大きな違いはあまりない。
LocalAllocは互換性維持のために残されていることから、特別な理由がない限り、AllocCoTaskMemを利用したほうが好ましい。」

そ、そんな… 今日まで AllocHGlobal を使ってきたのに… きっと AllocHGlobal にも良いところがあるはずだ!

@Nuits さんの処理速度ベンチマークでは 0.1msecオーダーで ぴったり同じ結果になっているが、usecオーダーなら AllocHGlobal の方が高速なはずだ! 頼むから何か取り柄があってくれ!!

ってことで BenchmarkDotNet を使って、処理時間を計測しました。

測定方法

@Nuits さんの計測で 100Byte だったメモリを10MByte に拡張してみました。

public class AllocateMemory
{
    private const int AllocSize = 10 * 1024 * 1024;

    [Benchmark]
    public void AllocHGlobal()
    {
        IntPtr intPtr = Marshal.AllocHGlobal(AllocSize);
        Marshal.FreeHGlobal(intPtr);
    }

    [Benchmark]
    public void AllocCoTaskMem()
    {
        IntPtr intPtr = Marshal.AllocCoTaskMem(AllocSize);
        Marshal.FreeCoTaskMem(intPtr);
    }
}

測定結果

Method Mean Error StdDev
AllocHGlobal 182.2 us 56.12 us 3.08 us
AllocCoTaskMem 179.4 us 21.47 us 1.18 us

元記事の通り大差はありませんでしたが、僅かに(1.5%) AllocCoTaskMem の方が高速でした。

互換性 だけでなく、処理速度の観点からも AllocCoTaskMem が優れていますので、AllocHGlobal の使用は今すぐ辞めましょう。

なんでこれまで AllocHGlobal を使ってたんだろうか。がっくり来ました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】Timelineの再生位置をスライダーで指定する

こんにちはっ?八ツ橋まろんですっ!

ARアプリを作っているときに、Timelineをスライダーで操作したくなったのでスクリプトを書きました。本記事ではそのコードと解説をします。

↓↓↓↓このコードによってこんなことができます↓↓↓↓(GIF)
(このGIFではアニメーションだけが入ったTimelineを使用しています)
Qiita.gif

・再生ボタン/一時停止ボタンの実装
・Timelineの進行度合いがスライダーに反映される
・スライダーの位置をクリックして変えるとTimelineの再生位置も連動する

以下、完成コード

TimelineUiController.cs
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Playables;
using System; // Actionに必要

public class TimelineUiController : MonoBehaviour
{
    [SerializeField]
    PlayableDirector timeline;

    [SerializeField]
    Slider slider;

    [SerializeField]
    GameObject playButton;

    [SerializeField]
    GameObject pauseButton;

    double length;

    float sliderValue;
    float sliderValueStored;
    float time;

    private void Start()
    {
        length = timeline.duration;
        Play();
    }

    void Update()
    {
        sliderValue = slider.value;
        time = ConvertDtoF(timeline.time);

        if(time >= 0.995 && timeline.extrapolationMode != DirectorWrapMode.Loop)
        {
            playButton.SetActive(true);
            pauseButton.SetActive(false);
        }

        if (sliderValue != sliderValueStored)
        {
            sliderValueStored = sliderValue;
            time = sliderValue;
            SetTimelineTime(time);
        }
        else if(time != sliderValueStored)
        {
            slider.value = time;
            sliderValueStored = time;
        }
    }

    public void Play()
    {
        timeline.Play();
        playButton.SetActive(false);
        pauseButton.SetActive(true);
    }

    public void Stop()
    {
        timeline.Stop();
        PlayOneFrame();
    }

    public void Pause()
    {
        timeline.Pause();

        time = ConvertDtoF(timeline.time);
        sliderValue = time;
        sliderValueStored = time;
        playButton.SetActive(true);
        pauseButton.SetActive(false);
    }
    public void LengthReset()
    {
        length = timeline.duration;
    }
    void PlayOneFrame()
    {
        Play();

        //1フレーム後にPauseする
        StartCoroutine(DelayMethod(1, () => {Pause(); }));
    }

    private IEnumerator DelayMethod(int delayFrameCount, Action action)
    {
        for (var i = 0; i < delayFrameCount; i++)
        {
            yield return null;
        }
        action();
    }

    void SetTimelineTime(float f)
    {
        double d = ConvertFtoD(f);
        timeline.time = d;
        PlayOneFrame();
    }

    double ConvertFtoD(float f)
    {
        double d = (double)(f * length);
        return d;
    }

    float ConvertDtoF(double d)
    {
        float f = (float)d / (float)length;
        return f;
    }
}


使い方

[SerializeField]
PlayableDirector timeline;

[SerializeField]
Slider slider;

[SerializeField]
GameObject playButton;

[SerializeField]
GameObject pauseButton;

上記にPlayableDirectorとスライダー、再生ボタン、停止ボタンをあてがってください。

解説

Timelineの総時間は

length = timeline.duration;

で取得できます。ただし、これはdouble型で得られるため、float型に変換しないといけないので注意。

float ConvertDtoF(double d)
{
    float f = (float)d / (float)length;
    return f;
}

timeline.timeで現在の再生位置をdouble型で取得し、総時間で割ったあとにfloat型に変換して、0~1にしています。

time = ConvertDtoF(timeline.time);

スライダーを触ってtimelineの再生位置を変更する場合、timeline.Pause();してtimeline.time = xxx;だけではスライダー位置が動かないため、その後に1フレームだけ再生することでスライダー位置を動かすようにしています。

おわりに

Timelineの再生位置制御の需要は、なかなかないと思いますが、Animation制御を越えてカメラワークやエフェクトの巻き戻しとかを任意でやりたい場合に使えます✨よかったら使ってみてください。

八ツ橋まろん

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

c# メモ

using System;
using System.Collections.Generic;
using System.Linq;

public class PlayerRepository
{
    private static readonly List<Player> Players = new List<Player>();

    public IReadOnlyList<Player> GetAll()
    {
        CheckPlayers();
        return new List<Player>(Players);
    }

    public void Regist(Player p)
    {
        CheckArguments(p);
        CheckRegistYet(p);
        Players.Add(p);
    }

    public void UnRegist(Player p)
    {
        CheckArguments(p);
        CheckRegistAlready(p);
        Players.Remove(p);
    }

    public bool Exists(Player p)
    {
        return Players.Contains(p);
    }

    private void CheckPlayers()
    {
        if (Players.Any(null))
            throw new Exception("Exist null in the list.");
    }

    private void CheckArguments(params object[] args)
    {
        foreach (var arg in args)
            CheckArgment(arg);
    }

    private void CheckArgment(object arg)
    {
        if (arg == null)
            throw new ArgumentNullException();
    }

    private void CheckRegistYet(Player p)
    {
        if (Exists(p))
            throw new Exception("Player is already registed");
    }

    private void CheckRegistAlready(Player p)
    {
        if (Exists(p))
            throw new Exception("Player is not registed");
    }
}

戻り値がリストならIReadOnlyList、リストをコピーして返す。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

for文とforeach文で配列の要素をすべて表示させる

class Program
{

    static void Main(string[] args)
    {
        string[] array = {"月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日" };

        //arrayの要素数を表示
        Console.WriteLine(array.Length);

        Console.WriteLine("");

        //要素数の数だけ繰り返す
        for (int i = 0;i <array.Length;i++)
        {
            Console.WriteLine(array[i]);
        }

        Console.WriteLine("");

        //foreachで要素数の数だけ繰り返す
        foreach (string a in array)
        {
            Console.WriteLine(a);
        }

    }
}

【結果】

7

月曜日
火曜日
水曜日
木曜日
金曜日
土曜日
日曜日

月曜日
火曜日
水曜日
木曜日
金曜日
土曜日
日曜日
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

配列

    class Program
    {

        static void Main(string[] args)
        {
            string[] array = {"月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日", "日曜日" };

            //arrayの要素数を表示
            Console.WriteLine(array.Length);

            //要素数の数だけ繰り返す
            for (int i = 0;i <array.Length;i++)
            {
                Console.WriteLine(array[i]);
            }

            Console.WriteLine("");

            //foreachで要素数の数だけ繰り返す
            foreach (string a in array)
            {
                Console.WriteLine(a);
            }

        }
    }

【結果】

7
月曜日
火曜日
水曜日
木曜日
金曜日
土曜日
日曜日

月曜日
火曜日
水曜日
木曜日
金曜日
土曜日
日曜日
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

while文で「10000」を「2」で何回割ると「100以下」になるか調べる

    class Program
    {

        static void Main(string[] args)
        {
            int count = 0;
            int kazu = 10000;

            while (kazu > 100)
            {
                kazu = kazu / 2;
                count++;
            }
            Console.WriteLine(count);
        }
    }

【結果】

7
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

for文のbreakとcontinueの使用例

class Program
{
    static void Main(string[] args)
    {
        //Program2のインスタンスを生成
        Program2 obj = new Program2();

        //Program2クラスのstarメソッドを実行
        obj.star();
    }
}

class Program2
{
    public void star()
    {
        //五回繰り返す
        for (int i=1;i<=5;i++)
        {
            //変数jが変数iの数以下になるまで繰り返す
            for (int j = 5; j >= i; j--)
            {
                //変数iが3の時はfor文を抜ける(Console.Write("☆")は実行されない)
                if (i == 3)
                {
                    break;
                }
                Console.Write("☆");
            }

            //変数iのが3の時はcontinue以下の処理は実行しない
            if (i == 3)
            {
                continue;
            }
            Console.WriteLine("");
        }
    }
}

【結果】

☆☆☆☆☆
☆☆☆☆
☆☆
☆
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#

    class Program
    {

        static void Main(string[] args)
        {
            Program2 obj = new Program2();
            obj.star();
            int sub(int a)
            {
                int b = a + 10;
                return b;
            }
        }




    }
    class Program2
    {
        public String helloworld()
        {
            string str = "Hello World!";
            return str;
        }

        public void star()
        {

            for (int i=1;i<=5;i++)
            {
                for (int j = 5; j >= i; j--)
                {

                    if (i == 3)
                    {
                        break;
                    }


                    Console.Write("☆");

                }

                if (i == 3)
                {
                    continue;
                }
                Console.WriteLine("");
            }
        }
    }

【結果】
☆☆☆☆☆
☆☆☆☆
☆☆

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# EntityFrameworkCore リレーションデータの取得、登録、削除

始めに

EntityFrameworkCoreを勉強がてらに触ってみて、「リレーションの構築」これは使えそうだなと思ったテクニックをメモしてみました。
作成したソースはメモ置き場としてgithubに公開しています。

開発環境

・.Net Core 3.1
MySql.Data.EntityFrameworkCore(8.0.18)
・Visual Studio Community 2019
・Windows 10

下準備

データベースへの接続環境とテーブルを構築します。

DB接続

MySQL用にDBの接続設定を行います。
MySQLの接続先は各々の環境で設定してください。

DbContextクラスを継承したクラスを作成し、「OnConfiguring」をオーバーライドします。

public class SampleEntities : DbContext
{
    // 一部省略

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        optionsBuilder.UseMySQL("MySQL接続文字列");
    }
}

データベース初期化

DBの接続先を変更したら下記コードでデータベースの作成を行います。

// 接続文字列記載のデータベースを削除
this.sampleEntities.Database.EnsureDeleted();

// 接続文字列記載のデータベースを作成
this.sampleEntities.Database.EnsureCreated();

※注意
「EnsureDeleted」は接続文字列記載のデータベースを全て削除します。今回は技術検証のため使用しています。
使い方等についてはApi の作成と削除-EF Core | Microsoft Docsを参照ください。

リレーションの構築

データベースの初期化を行うと次の4つのテーブルを作成します。ER図作って
PKは全て「ID」としオートインクリメントで構成します。
・M_AREA (MArea.cs)
・M_SHOP (MShop.cs) ・・・ M_AREAが1に対して複数存在
・T_DAILY_SALES (TDailySales.cs) ・・・ M_SHOPが1に対して複数存在
・T_MONTHLY_SALES (TMonthlySales.cs) ・・・ M_SHOPが1に対して複数存在

関連データの構築はDbContextを継承したクラスの「OnModelCreating」の中で行います。

public class SampleEntities : DbContext
{
    public DbSet<MArea> Areas { get; set; }

    public DbSet<MShop> Shops { get; set; }

    public DbSet<TDailySales> DailySales { get; set; }

    public DbSet<TMonthlySales> MonthlySales { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
        optionsBuilder.UseMySQL("MySQL接続文字列");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // エリアと店舗情報で1:nの関係を作成
        modelBuilder.Entity<MShop>()
            .HasOne(s => s.Area)
            .WithMany(a => a.Shops);

        // 店舗情報と売上高(日別)で1:nの関係を作成
        modelBuilder.Entity<TDailySales>()
            .HasOne(d => d.Shop)
            .WithMany(s => s.SalesDailies);

        // 店舗情報と売上高(月別)で1:nの関係を作成
        modelBuilder.Entity<TMonthlySales>()
            .HasOne(m => m.Shop)
            .WithMany(s => s.SalesMonthlies);
    }
}

リレーション構築に使用するエンティティクラスです。

[Table("M_AREA")]
public class MArea
{
    [Key]
    [Column("ID")]
    public int Id { get; set; }

    /* 長くなるため一部プロパティは省略 */

    /// <summary>
    /// 店舗情報
    /// </summary>
    public List<MShop> Shops { get; set; }
}

[Table("M_SHOP")]
public class MShop
{
    [Key]
    [Column("ID")]
    public int Id { get; set; }

    // 長くなるため一部プロパティは省略

    /// <summary>
    /// エリアID
    /// </summary>
    [Column("AREA_ID")]
    public int AreaId { get; set; }

    /// <summary>
    /// エリア
    /// </summary>
    public MArea Area { get; set; }

    /// <summary>
    /// 売上高(日別)
    /// </summary>
    public List<TDailySales> SalesDailies { get; set; }

    /// <summary>
    /// 売上高(月別)
    /// </summary>
    public List<TMonthlySales> SalesMonthlies { get; set; }
}

[Table("T_SALES_DAILY")]
public class TDailySales
{
    [Key]
    [Column("ID")]
    public int Id { get; set; }

    // 長くなるため一部プロパティは省略

    /// <summary>
    /// 店舗ID
    /// </summary>
    [Column("SHOP_ID")]
    public int ShopId { get; set; }

    /// <summary>
    /// 店舗情報
    /// </summary>
    public MShop Shop { get; set; }
}

[Table("T_SALES_MONTHLY")]
public class TMonthlySales
{
    [Key]
    [Column("ID")]
    public int Id { get; set; }

    // 長くなるため一部プロパティは省略

    /// <summary>
    /// 店舗ID
    /// </summary>
    [Column("SHOP_ID")]
    public int ShopId { get; set; }

    /// <summary>
    /// 店舗情報
    /// </summary>
    public MShop Shop { get; set; }
}

リレーション構築に必要なプロパティ名

リレーションを構築する上でプロパティの名前が重要になってきます。
プロパティ名がパターンに則っていない場合、リレーションは構築されません。
そのパターンは4つあり、1つ前のセクションで説明した「MShop」クラスで構築した場合の具体例と共にメモします。

パターン1:<navigation property name><principal key property name>

// このプロパティが「navigation property」
public MArea Area { get; set; }

// 「Area」が「navigation property name」、「Id」が「principal key property name」
[Column("AREA_ID")]
public int AreaId { get; set; }

パターン2:<navigation property name>Id

// このプロパティが「navigation property」
public MArea Area { get; set; }

// 「Area」が「navigation property name」になり、その後ろに「Id」をつける。
[Column("AREA_ID")]
public int AreaId { get; set; }

パターン3:<principal entity name><principal key property name>

public MArea Area { get; set; }

// 「MArea」が「principal entity name」、「Id」が「principal key property name」
[Column("AREA_ID")]
public int MAreaId { get; set; } 

パターン4:<principal entity name>Id

public MArea Area { get; set; }

// 「MArea」が「principal entity name」になり、その後ろに「Id」をつける。
[Column("AREA_ID")]
public int MAreaId { get; set; }

上記パターンの詳細な説明についてはリレーションシップ-EF Core | Microsoft Docsを参照してください。
ちなみに1対多のほかにで1対1、多対多でのリレーションの組み方などが書いてあります。

リレーションデータ取得、登録、削除

リレーションデータ一括取得

リレーションデータの取得は次のように行います。

using (SampleEntities sampleEntities = new SampleEntities())
{
    // エリア、店舗、売上情報取得
    MArea area = sampleEntities.Areas
        .Include(a => a.Shops)
            .ThenInclude(shop => shop.SalesDailies)
        .Include(a => a.Shops)
            .ThenInclude(shop => shop.SalesMonthlies)
            .FirstOrDefault();

}

リレーションデータの構築を行っていない場合の取得方法は「Join」や「Linqのクエリ式」があります。
ただ今回はリレーションデータに関するメモのため省きます。。

リレーションデータ一括登録

リレーションを構築するとオートインクリメントをキーに使用するデータの登録が簡単になります。
「LAST_INSERT_ID()」をINSERT文に組み込むなどの工夫が必要でしたが(私はこんな感じにやってました)、リレーションを構築すると、主キーを自動で紐づけて登録を行ってくれます。

// エリア作成
MArea area1 = new MArea { AreaName = "北海道" };

// エリアに店舗情報追加
MShop shopHokkaido = new MShop { Area = area1, Address = "北海道xxxxxxxxxx", ShopName = "北海道店舗" };

// 店舗に売上情報追加:北海道
shopHokkaido.SalesDailies = new List<TDailySales>();
shopHokkaido.SalesDailies.Add(new TDailySales { Shop = shopHokkaido, /*紐づけに必要な情報以外は省略*/ });
shopHokkaido.SalesDailies.Add(new TDailySales { Shop = shopHokkaido, /*紐づけに必要な情報以外は省略*/ });
shopHokkaido.SalesDailies.Add(new TDailySales { Shop = shopHokkaido, /*紐づけに必要な情報以外は省略*/ });

shopHokkaido.SalesMonthlies = new List<TMonthlySales>();
shopHokkaido.SalesMonthlies.Add(new TMonthlySales { Shop = shopHokkaido, /*紐づけに必要な情報以外は省略*/ });

using(SampleEntities sampleEntities = new SampleEntities())
{
    // 登録内容の追加
    sampleEntities.Areas.Add(area1);
    sampleEntities.Shops.Add(shopHokkaido);

    // 登録内容の保存
    sampleEntities.SaveChanges();
}

ちなみに登録前の何も設定していないIDの値を確認すると「-2147482647」から連番になっていることができます。
登録内容の保存後は採番された値が代入されていることを確認できます。

リレーションデータ一括削除

リレーションデータの物理削除を行います。
リレーションの親要素を削除することで、子要素も自動で削除されます。

using(SampleEntities sampleEntities = new SampleEntities())
{
    // エリア情報取得
    // 子要素を取得しなくても、リレーションデータの削除されることを確認。
    MArea area = sampleEntities.Areas.FirstOrDefault();
        //.Include(a => a.Shops)
        //    .ThenInclude(shop => shop.SalesDailies)
        //.Include(a => a.Shops)
        //    .ThenInclude(shop => shop.SalesMonthlies)
        //    .FirstOrDefault();

    // DbSet経由の削除
    sampleEntities.Remove(area);

    // 削除内容の保存
    sampleEntities.SaveChanges()
}

参考にしたページ

「EF Core | Microsoft Docs」の下記ページを主に参考にしました。
リレーションシップ
関連データの読み込み
関連データの保存
連鎖削除

最後に

「EF Core| Microsoft Docs」にはまだまだ有用なテクニック書かれているため、本格的に業務で使うとなれば理解する必要があると感じました。
データベースの移行に関するテクニックは特に。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SeleniumのChromeDriverでChromium版MicrosoftEdgeを動かす

はじめに

ブラウザ自動テストのデファクトスタンダードであるSelenium
そして正式リリースされたChromium版MicrosoftEdge
はたしてChromeDriverを使ってChromium版MicrosoftEdgeを操作することができるのか?

動かせた時の感動は省略

やったこと

Chromium版MicrosoftEdgeの取得

こちらからダウンロード&インストール

NuGet

Selenium系のモジュール参照
image.png

ソース

var options = new ChromeOptions
{
    // 結局、いつもと違うのはここだけ!
    BinaryLocation = @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
};

using (var driver = new ChromeDriver())
{
    var wait = new WebDriverWait(driver, new TimeSpan(0, 0, 5));
    driver.Navigate().GoToUrl("https://www.google.com");
    driver.FindElementByName("q").SendKeys("Chromium");
    driver.FindElementByName("q").Submit();

    wait.Until(ExpectedConditions.TitleIs("Chromium - Google 検索"));
    ((ITakesScreenshot)driver).GetScreenshot().SaveAsFile($"{DateTime.Now.ToString("yyyyMMddHHmmss")}.png");
}

ヘッドレスは未対応?

現時点で下記記述によるヘッドレスモードでの起動は未対応。
普通にブラウザが表示されて実行される。

options.AddArgument("--headless");

おわりに

BinaryLocationを直接指定する必要があるので環境ごとに変更する必要が出てくる可能性がある。
ヘッドレスモードなど、随時サポートされてくることを期待。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WinForms で「値をNull にすることはできません。パラメーター名:source」な場合に見直す点

WinForms でアプリケーションを開発していて、(閉じる前までは) デザイナで開けていたフォームが開けなくなり、次のエラーが報告されるケースで見直す点をメモする。

値をNull にすることはできません。パラメーター名:source

もしくは

オブジェクト参照がオブジェクト インスタンスに設定されていません。

Visual Studio 2015 は確か上のエラー、同 2019 では下のエラーメッセージになった。

見直す点

このエラーは、ArgumentNullException を受け取ったデザイナが表示している。

デザイナがフォームを表示するとき、フォームに貼り付けられたすべてのコントロールをインスタンス化、そして Load も呼び出す。その過程で、誰かが 引数に null を与えてはいけないメソッドに null を与えた ことで例外がスローされた。

したがって、フォームの Load 時に呼び出されるイベントのコールバックを見直せばよい。

サンプル

次に、端的なサンプルと再現手順を挙げる。

  1. Visual Studio で WinForms プロジェクトを生成して、プロジェクトにカスタムコントロールを追加してフォームに貼り付ける。
  2. 貼り付けた後に、次のコメントアウト部分を有効にしてフォームを保存する。
  3. フォームを閉じて、開きなおす。
  4. 上記のエラーが発生する。
public partial class UserControl1 : UserControl
{
    // 前略

    IEnumerable<string> s;

    private void UserControl1_Load(object sender, EventArgs e)
    {
        //if (s.Count() > 0)
        //{
        //    // somthing...
        //}
    }
}

エラーメッセージの パラメータ名: 以降に表示されるのは、例外をスローしたメソッドが Null を検出した仮引数名のようだ。

確認した環境

  • Visual Studio: Microsoft Visual Studio Community 2019, Microsoft Visual Studio Community 2015
  • Windows: Windows 10 Home
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでListの最小値を取得する最速の方法

A*経路探索のコードを書いていてListの最小値を取得する最速の方法が気になったので検証してみました。
前回、速度比較のやり方を模索してみたのはこの為だったりします。

for文 vs List.Min

普通にfor文で全要素を一つづつ比較していくのとList.Minではどっちが速いんでしょうか?
ついでにLinqのOrderByしてFirstした場合も比較してみます。

コードは以下です。
0~100000のランダムな整数の入った要素数10万のListを宣言してその中から最小値を取得、
Profiler.BeginSample/EndSampleで、最小値を取得する部分だけを計測しています。

public class MinTest {

    //for文のやつ
    [Test]
    public void UseFor() {
        var randamValues = GetRandamValues();
        Profiler.BeginSample("MinTest.UseFor");
        var min = randamValues[0];
        for (int i = 1; i < randamValues.Count; i++) {
            min = randamValues[i] < min ? randamValues[i] : min;
        }
        Profiler.EndSample();
        Debug.Log(min);
    }

    //ListMin
    [Test]
    public void UseListMin() {
        var randamValues = GetRandamValues();
        Profiler.BeginSample("MinTest.UseListMin");
        var min = randamValues.Min();
        Profiler.EndSample();
        Debug.Log(min);
    }

    //OrderByしてFirst
    [Test]
    public void UseOrderByFirst() {
        var randamValues = GetRandamValues();
        Profiler.BeginSample("MinTest.UseOrderByFirst");
        var min = randamValues.OrderBy(x => x).First();
        Profiler.EndSample();
        Debug.Log(min);
    }

    List<int> GetRandamValues() {
        var randamValues = new List<int>();
        for (int i = 0; i < 100000; i++) {
            randamValues.Add(Random.Range(0, 100000));
        }
        return randamValues;
    }
}

結果

GC Alloc Time
UseFor 0B 1.18ms
UseListMin 40B 1.49ms
UseOrderByFirst 136B 3.92ms

for文 vs List.Minは速度には気にするほどの差は出ませんが、List.MinはGC Allocが発生してしまいますね。
Linqは普通に遅かった・・・

条件分岐の書き方で差は出るの?

やっぱり普通にfor文使うのが速いですね。
しかし、もう一つ気になることがあります。
条件分岐の書き方で差が出るかどうかです。

//for文のやつ
    [Test]
    public void UseFor() {
        var randamValues = GetRandamValues();
        Profiler.BeginSample("MinTest.UseFor");
        var min = randamValues[0];
        for (int i = 1; i < randamValues.Count; i++) {
            min = randamValues[i] < min ? randamValues[i] : min;//←ここの書き方で差は出るの?
        }
        Profiler.EndSample();
        Debug.Log(min);
    }

以下の3つを試してみます

①if文

if (randamValues[i] < min) min = randamValues[i];

②条件演算子

min = randamValues[i] < min ? randamValues[i] : min;

③条件演算子(条件式を反転)

min = randamValues[i] >= min ? min : randamValues[i];

結果

誤差で結果が上下するので、1000回の平均を計測しました。
微妙に差が出ますが、気にするレベルではないですね。

GC Alloc Time
①if文 0B 1.28120ms
②条件演算子 0B 1.26898ms
③条件演算子(条件式を反転) 0B 1.24765ms

まとめ

普通にfor文使うのが速いしGC Allocも発生しない!

ご意見ご感想、多分これが一番速いと思います等ありましたらコメントを頂けますと幸いです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む