for WPF developers
Home Profile Tips 全記事一覧

シーケンスを n 個毎に分割する

(2017/06/28 9:31:13 created.)

(2017/06/28 11:30:35 modified.)

実務で必要になったのでやってみた内容です。 要求仕様はいたってシンプルで、平坦に並んでいるシーケンスを n 個毎に区切ってグルーピングすることが目的です。

例えば { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } を 3 個毎に分割して、{ { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 }, { 9 } } という 4 つの要素に分割したシーケンスにします。最後の { 9 } については { 9, 0, 0 } というように既定値で詰めたりとかいろいろあるかもしれませんが、ここではある分だけの要素をそのままにしておくようにします。

で、結局 stackoverflow にお世話になりました。
ところが、方法がいくつも書いてあって、しかも ToArray() したら不具合のあるものもあって結局どれを使えばいいの?ってなったので、自分で処理時間を評価してみました。

まず時間計測用のクラスを用意します。ここでは tocsworld | C# での処理時間計測いろいろ を参考にして、Win32 API を使用する方法を採用しました。

TimeCount.cs
  1. namespace ConsoleApplication2
  2. {
  3.     using System.Runtime.InteropServices;
  4.  
  5.     internal class TimeCount
  6.     {
  7.         [DllImport("kernel32.dll")]
  8.         private static extern bool QueryPerformanceCounter(ref long lpPerformanceCount);
  9.  
  10.         [DllImport("kernel32.dll")]
  11.         private static extern bool QueryPerformanceFrequency(ref long lpFrequency);
  12.  
  13.         private long _startCounter;
  14.  
  15.         public void Start()
  16.         {
  17.             QueryPerformanceCounter(ref this._startCounter);
  18.         }
  19.  
  20.         public void Reset()
  21.         {
  22.             QueryPerformanceCounter(ref this._startCounter);
  23.         }
  24.  
  25.         public double ElapsedMilliseconds
  26.         {
  27.             get
  28.             {
  29.                 long stopCounter = 0;
  30.                 QueryPerformanceCounter(ref stopCounter);
  31.                 long frequency = 0;
  32.                 QueryPerformanceFrequency(ref frequency);
  33.                 return frequency != 0 ? (double)(stopCounter - this._startCounter) * 1000.0 / frequency : 0;
  34.             }
  35.         }
  36.     }
  37. }

そして、今回比較するメソッドは次の 3 つです。

Extensions.cs
  1. namespace ConsoleApplication2
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Linq;
  6.  
  7.     public static class Extensions
  8.     {
  9.         public static IEnumerable<IEnumerable<T>> Chunk1<T>(this IEnumerable<T> source, int size)
  10.         {
  11.             if (source == null) throw new ArgumentException("source");
  12.             if (size < 1) throw new ArgumentException("size");
  13.  
  14.             return source.Select((x, i) => new { Index = i, Value = x })
  15.                          .GroupBy(x => x.Index / size, (key, result) => result.Select(r => r.Value));
  16.         }
  17.  
  18.         public static IEnumerable<IEnumerable<T>> Chunk2<T>(this IEnumerable<T> source, int size)
  19.         {
  20.             if (source == null) throw new ArgumentException("self");
  21.             if (size < 1) throw new ArgumentException("size");
  22.  
  23.             while (source.Any())
  24.             {
  25.                 yield return source.Take(size);
  26.                 source = source.Skip(size);
  27.             }
  28.         }
  29.  
  30.         public static IEnumerable<IEnumerable<T>> Chunk3<T>(this IEnumerable<T> source, int size)
  31.         {
  32.             if (source == null) throw new ArgumentException("self");
  33.             if (size < 1) throw new ArgumentException("size");
  34.  
  35.             using (var enumerator = source.GetEnumerator())
  36.             {
  37.                 var list = new List<T>(size);
  38.                 while (enumerator.MoveNext())
  39.                 {
  40.                     list.Add(enumerator.Current);
  41.                     if (list.Count >= size)
  42.                     {
  43.                         yield return list;
  44.                         list = new List<T>();
  45.                     }
  46.                 }
  47.  
  48.                 // 残りの部分
  49.                 if (list.Any())
  50.                 {
  51.                     yield return list;
  52.                 }
  53.             }
  54.         }
  55.     }
  56. }

Chunk1<T>() 拡張メソッドは GroupBy() 拡張メソッドを利用したスマートな方法です。Select() 拡張メソッドで 2 回射影をおこなってはいますが、処理時間のオーダーとしては O(n) になっています。GroupBy() 拡張メソッドもハッシュ値による処理をおこなっているため、それほど遅くはならないという予測です。

Chunk2<T>() 拡張メソッドは Take() 拡張メソッドと Skip() 拡張メソッドの組み合わせで、非常に直感的なコードになっています。しかし、一度のループの中で 2 回全要素を走査しているため、処理時間のオーダーは O(n^2) となります。

Chunk3<T>() 拡張メソッドは Linq をあきらめて泥臭くした方法です。IEnumerator<T> を使って先頭要素から順番に指定個数分を List<T> に入れていく方法です。こちらの処理時間のオーダーは O(n) となります。

というわけで、もう結果は見えているような気がしますが、念のため比較をしてみます。

Program.cs
  1. namespace ConsoleApplication2
  2. {
  3.     using System;
  4.     using System.Collections.Generic;
  5.     using System.Linq;
  6.  
  7.     class Program
  8.     {
  9.         static void Main(string[] args)
  10.         {
  11.             var func = new Action<IEnumerable<int>>(sequence =>
  12.             {
  13.                 Console.WriteLine("length = {0} ------------------", sequence.Count());
  14.  
  15.                 var size = 13;
  16.                 double elapsed;
  17.                 var counter = new TimeCount();
  18.  
  19.                 counter.Start();
  20.                 var chunk1 = sequence.Chunk1(size).ToArray();
  21.                 elapsed = counter.ElapsedMilliseconds;
  22.                 Console.WriteLine("Chunk1 : {0, 10:f3}[ms] : {1} : {2}", elapsed, string.Join(" ", chunk1.First().Select(x => x.ToString())), string.Join(" ", chunk1.Last().Select(x => x.ToString())));
  23.  
  24.                 counter.Reset();
  25.                 var chunk2 = sequence.Chunk2(size).ToArray();
  26.                 elapsed = counter.ElapsedMilliseconds;
  27.                 Console.WriteLine("Chunk2 : {0, 10:f3}[ms] : {1} : {2}", elapsed, string.Join(" ", chunk2.First().Select(x => x.ToString())), string.Join(" ", chunk2.Last().Select(x => x.ToString())));
  28.  
  29.                 counter.Reset();
  30.                 var chunk3 = sequence.Chunk3(size).ToArray();
  31.                 elapsed = counter.ElapsedMilliseconds;
  32.                 Console.WriteLine("Chunk3 : {0, 10:f3}[ms] : {1} : {2}", elapsed, string.Join(" ", chunk3.First().Select(x => x.ToString())), string.Join(" ", chunk3.Last().Select(x => x.ToString())));
  33.  
  34.                 Console.WriteLine("");
  35.             });
  36.  
  37.             var rnd = new Random();
  38.             do
  39.             {
  40.                 Console.Clear();
  41.  
  42.                 var source = Enumerable.Range(0, 10000).Select(x => rnd.Next(0, 101)).ToArray();
  43.                 func(source.Take(1000).ToArray());
  44.                 func(source.Take(2000).ToArray());
  45.                 func(source.Take(3000).ToArray());
  46.                 func(source.Take(4000).ToArray());
  47.                 func(source.Take(5000).ToArray());
  48.                 func(source);
  49.             } while (Console.ReadKey().Key != ConsoleKey.Escape);
  50.         }
  51.     }
  52. }

func 変数で 3 つの拡張メソッドを実行するメソッドを準備して、 色々な長さのシーケンスを処理させます。実行結果はこちら。


予想通り Chunk3<T>() 拡張メソッドが一番速い結果となりました。Chunk2<T>() 拡張メソッドは遅いとは思っていましたが、予想を超えてはるかに遅い…。控えめに言って使い物になりませんね。

以上、シーケンスを n 個毎に分割する方法でした。Linq は油断すると処理が遅くなるので要注意。