*現状
現在C#にてプログラミングをしているのですが、どうしても処理の遅さがネックとなって先に進めない状態が続いているため質問させていただきました。
*質問内容
膨大な量のリストの中から不要な項目を削除する処理を高速化する方法があればご教示お願いします。
*要件
- 約600万件近いデータのリストがあり、その中から不要な項目を削除したいと考えております。
- 削除したい件数も50万件近くあります。
- 削除したいデータが必ずしも600万件のリストの中に存在するわけではないので、1つづつ存在するかを確認しなければなりません。
- 1件1件のデータ自体は文字列で、例えば「花太郎」、「小太郎」みたいな文字列となっています。
*問題点
データ内容自体は異なるのですが、下記のコードでも処理に約1時間ほどかかっています。
*コード内容
データ内容を単純化するためにデータを文字数字にしています。
C#
1static void Main(string[] args) 2{ 3 // 600万件分のリストを作成 4 List<string> mainList = new List<string>(); 5 for(int i=0; i<6000000; i++) 6 { 7 mainList.Add(i.ToString()); 8 } 9 10 // 50万件分のリストを作成 11 List<string> deleteList = new List<string>(); 12 for(int i=0; i<500000; i++) 13 { 14 deleteList.Add(i.ToString()); 15 } 16 17 // mainListの中にdeleteListの項目があれば削除する 18 // ここの処理を高速化できないかを悩み中です。 19 foreach (string deleteStr in deleteList) 20 { 21 mainList.Remove(deleteStr); 22 } 23}
*補足
データベースが利用できない状況なので、リストで処理を行っています。
処理が早くできればList<string>である必要はないです。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

回答9件
0
ベストアンサー
Listからの削除は大変重い処理です。これはListが「配列(array)」と言われるデータ構造であるため、インデックスアクセスがO(1)になる代わりに、削除がO(n)になってしまうからです。ましてや、Removeで添字ではなくオブジェクトを指定する場合は、検索もO(n)になるため、さらに遅い原因になります。
つまり、オリジナルコードは削除のリスト分繰り返しているためO(n × m)です。(nはmainのサイズ、mはdeleteのサイズ)
解決策は二つ(順序重複維持あり)+二つ(順序重複維持なし)ほどです。
###順序と重複を維持する場合
順序を維持し、また、重複している場合は重複したままにする場合です。
####連結リストを使う
「連結リスト(linked list)」といわれるデータ構造の場合は、削除はO(1)になります(代わりに、インデックスアクセスがO(n)です)。C#では連結リストとしてLinkedListが用意されていますので、こちらを使用すれば、速度アップになります。
と言いたいところですが、この方法ではそれほど速くなりません。
なぜなら、Removeで該当のオブジェクトを探す検索がO(n)の計算量が必要になるからです。削除が速いだけで見つけるのが速くなるわけではありません。結局O(n × m)になってしまいます。
####新たに配列を作る
削除するのではなく、削除対象ではない物だけ抜き出して、新しい配列にします。処理はLINQを使うとコードがすっきりするでしょう。また、削除のリストにHashSetを使うと削除対象かどうかのチェックも高速に行うことができます。
HashSetの作成はO(n)ですが、検索はO(1)です。配列作成も内部がO(1)ならO(n)になりますので、計算量はO(n + m)とかなり改善されます。
###順序と重複を維持しない場合
順序を維持し、また、重複している場合は重複したままにする場合です。
####並び替え済みリストを使う
並び替え済みリストの場合、検索がO(1)と高速になります。C#には並び替え済みのSortedListがありますのでこれが使用できます。ただし、削除はO(n)であり、オリジナルより速い程度です。結局O(n × m)になってしまいます。
重複は維持されますが、順序はソートされるために維持できません。
####ハッシュ化された集合を使う
順序や重複を維持しなければいいのであれば、ハッシュ化された集合が使用できます。検索も削除もほぼO(1)になるため、極めて高速です。
ハッシュ化された集合としてHashSetが使用できます。なお、HashSetでは順序もそのまま維持されるようです(正式ドキュメントが見つけられなかったので、知っている人は教えて欲しい)。ただし、重複は維持されません。
ただし、ハッシュ化された集合自体の作成にO(n)必要になりますので、そこは注意してください。ということで、計算量はO(n + m)とかなり改善されます。。
###検証コード
C#
1using System; 2using System.Collections.Generic; 3using System.Diagnostics; 4using System.Linq; 5 6class MainClass 7{ 8 static void Main(string[] args) 9 { 10 //int mainListSize = 6000000; 11 //int deleteListSize = 500000; 12 int mainListSize = 60000; 13 int deleteListSize = 5000; 14 15 Console.WriteLine("{0}個中{1}個を削除", 16 mainListSize, deleteListSize); 17 18 // 600万件分のリストを作成 19 var mainStopwatch = new Stopwatch(); 20 mainStopwatch.Start(); 21 var mainList = Enumerable.Range(0, mainListSize) 22 .Select(i => i.ToString()) 23 .OrderBy(a => Guid.NewGuid()) 24 .ToList(); 25 mainStopwatch.Stop(); 26 Console.WriteLine("main 作成完了: {0} ms", 27 mainStopwatch.ElapsedMilliseconds); 28 29 // 50万件分のリストを作成 30 var deleteStopwatch = new Stopwatch(); 31 deleteStopwatch.Start(); 32 var deleteList = Enumerable.Range(0, deleteListSize) 33 .Select(i => i.ToString()) 34 .OrderBy(a => Guid.NewGuid()) 35 .ToList(); 36 deleteStopwatch.Stop(); 37 Console.WriteLine("delete 作成完了: {0} ms", 38 deleteStopwatch.ElapsedMilliseconds); 39 40 Console.WriteLine(); 41 42 List<string> list; 43 44 list = Original(new List<String>(mainList), 45 new List<String>(deleteList)); 46 Console.WriteLine("サイズ: {0}", list.Count); 47 Console.WriteLine("最初: {0}", list[0]); 48 49 Console.WriteLine(); 50 51 list = UseLinkedList(new List<String>(mainList), 52 new List<String>(deleteList)); 53 Console.WriteLine("サイズ: {0}", list.Count); 54 Console.WriteLine("最初: {0}", list[0]); 55 56 Console.WriteLine(); 57 58 list = CreateListWithHashset(new List<String>(mainList), 59 new List<String>(deleteList)); 60 Console.WriteLine("サイズ: {0}", list.Count); 61 Console.WriteLine("最初: {0}", list[0]); 62 63 Console.WriteLine(); 64 65 list = UseSortedList(new List<String>(mainList), 66 new List<String>(deleteList)); 67 Console.WriteLine("サイズ: {0}", list.Count); 68 Console.WriteLine("最初: {0}", list[0]); 69 70 Console.WriteLine(); 71 72 list = UseHashSet(new List<String>(mainList), 73 new List<String>(deleteList)); 74 Console.WriteLine("サイズ: {0}", list.Count); 75 Console.WriteLine("最初: {0}", list[0]); 76 } 77 78 static List<String> Original(List<String> mainList, 79 List<String> deleteList) 80 { 81 Console.WriteLine("start Original"); 82 var runStopwatch = new Stopwatch(); 83 runStopwatch.Start(); 84 85 // mainListの中にdeleteListの項目があれば削除する 86 // ここの処理を高速化できないかを悩み中です。 87 foreach (string deleteStr in deleteList) 88 { 89 mainList.Remove(deleteStr); 90 } 91 92 runStopwatch.Stop(); 93 Console.WriteLine("run 完了: {0} ms", 94 runStopwatch.ElapsedMilliseconds); 95 return mainList; 96 } 97 98 static List<String> UseLinkedList(List<String> mainList, 99 List<String> deleteList) 100 { 101 Console.WriteLine("start UseLinkedList"); 102 var runStopwatch = new Stopwatch(); 103 runStopwatch.Start(); 104 105 // LinkedListに変換して処理をする。 106 var mainLinkedList = new LinkedList<String>(mainList); 107 foreach (string deleteStr in deleteList) 108 { 109 mainLinkedList.Remove(deleteStr); 110 } 111 var newList = mainLinkedList.ToList(); 112 113 runStopwatch.Stop(); 114 Console.WriteLine("run 完了: {0} ms", 115 runStopwatch.ElapsedMilliseconds); 116 return newList; 117 } 118 119 static List<String> CreateListWithHashset(List<String> mainList, 120 List<String> deleteList) 121 { 122 Console.WriteLine("start CreateListWithHashset"); 123 var runStopwatch = new Stopwatch(); 124 runStopwatch.Start(); 125 126 // 削除用のHashSetを作成して、処理をする。 127 var deleteSet = new HashSet<String>(deleteList); 128 var newList = mainList 129 .Where((s) => !deleteSet.Contains(s)).ToList(); 130 131 runStopwatch.Stop(); 132 Console.WriteLine("run 完了: {0} ms", 133 runStopwatch.ElapsedMilliseconds); 134 return newList; 135 } 136 137 static List<String> UseSortedList(List<String> mainList, 138 List<String> deleteList) 139 { 140 Console.WriteLine("start UseSortedList"); 141 var runStopwatch = new Stopwatch(); 142 runStopwatch.Start(); 143 144 // SortedListに変換して処理をする。 145 var mainSortedList = new SortedList<String, String>( 146 mainList.ToDictionary(s => s)); 147 foreach (string deleteStr in deleteList) 148 { 149 mainSortedList.Remove(deleteStr); 150 } 151 var newList = mainSortedList.Select(pair => pair.Value) 152 .ToList(); 153 154 runStopwatch.Stop(); 155 Console.WriteLine("run 完了: {0} ms", 156 runStopwatch.ElapsedMilliseconds); 157 return newList; 158 } 159 160 static List<String> UseHashSet(List<String> mainList, 161 List<String> deleteList) 162 { 163 Console.WriteLine("start UseHashSet"); 164 var runStopwatch = new Stopwatch(); 165 runStopwatch.Start(); 166 167 // HashSetに変換して処理をする。 168 var mainHashSet = new HashSet<String>(mainList); 169 foreach (string deleteStr in deleteList) 170 { 171 mainHashSet.Remove(deleteStr); 172 } 173 var newList = mainHashSet.ToList(); 174 175 runStopwatch.Stop(); 176 Console.WriteLine("run 完了: {0} ms", 177 runStopwatch.ElapsedMilliseconds); 178 return newList; 179 } 180 181}
データの並びはランダム化しています。連結リストの場合は削除するデータがどこにあるかによって処理時間が大きく変わるからです。
600万件中50万件だとオリジナルや連結リストが遅すぎるので、6万件中5千件での実行結果です。
60000個中5000個を削除 main 作成完了: 368 ms delete 作成完了: 26 ms start Original run 完了: 4572 ms サイズ: 55000 最初: 57539 start UseLinkedList run 完了: 3258 ms サイズ: 55000 最初: 57539 start CreateListWithHashset run 完了: 13 ms サイズ: 55000 最初: 57539 start UseSortedList run 完了: 2628 ms サイズ: 55000 最初: 10000 start UseHashSet run 完了: 16 ms サイズ: 55000 最初: 57539
UseSortedListの最初の要素は異なります。UseHashSetの場合は重複が維持されないことに注意です。なお、順序づけしておきたい場合はSortedSetを使うという手段もあります。
投稿2017/06/11 01:11
編集2017/06/11 02:34総合スコア21741
0
こんにちは。
コレクションの内部実装を見るとList<T>ってソートしてないので頭から1つ1つ比較するので遅いです。更に連続したメモリを確保するようなのでRemoveも遅そうです。(多量のメモリコピーが発生するため)
mainListの数は最初600万、最後550万ですね。検索で見つかるのに平均して全体の半分と比較したと仮定すると最初平均300万回、最後は275万回の比較を行います。更に平均して287.5万回です。
これを50万回繰り返すので、287.5万回x50万回の比較を行います。
mainListをSortedSet<T>に変更すると、2分探索になりますので、600万個(<2の23乗)から見つける時に必要な比較回数は23回です。それを50万回繰り返します。
287.5万回/23回なので125,000倍の速度アップを期待できます。
SortedSet<T>は一般に言うリスト構造なのでRemoveは高速ですので、更に速くなる筈。
ご提示されたコードは600万個の頭の50万個に削除するものが集中しているから、1削除当たり平均25万回の比較になるため、25万回x50万回の比較になりますね。
ですので、25万回/23回なので1万倍程高速化することが期待できます。1秒かからない筈。
と思ったけど違いますね。常に削除対象が先頭になるので、1削除当たり1回の比較しかしてないはずですね。
となると比較回数は確実に増えます。ご提示のソースの場合、Remove処理がほぼ全てです。リスト構造になることでRemoveは高速になりますから、速度が上がる可能性もあります。やってみないと解らないです。
なお、実データの場合は、こんな極端な偏りがないなら高速化できる筈です。
すいません。実際にはやったことないので、上記は理論値です。
計算間違いもあるかも知れません。違ってたらごめんなさい。
投稿2017/06/10 17:34
編集2017/06/10 17:49総合スコア23274
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
どうしてもオンメモリで処理しなければならないのでしょうか?
処理対象のデータがデータベースに格納されているなら、データベースの機能をつかってデータの操作を行った方が良いように思います。
投稿2017/06/11 03:15
総合スコア403
0
List<T>は連続領域に要素を順番に配置した配列のような構造となっているため、「1件ずつ削除する」のは非常にコストの高い処理になるのは他の方がおっしゃるとおりと思います。途中の要素を削除した場合、それ以降の要素をひとつずつ前に「詰めなければならない」ので処理時間は詰めなければならない要素の数に比例=(リストのサイズに比例)するのでO(n)ということになります。
よって削除しなければならない要素を一つずつRemoveすると大変遅くなりますが、ListにはRemoveAllというメソッドも用意されています。このメソッドはRemoveを繰り返し呼び出すような実装にはなっていません。以下のリファレンスの備考にあるように計算量はO(n)です。
RemoveAllが内部でRemoveの繰り返しにより実装されているのであれば処理時間はO(n^2)相当になるはずですがO(n)になっているのは、複数の要素を削除する際、削除の度にそれ以降の要素を一つずつずらすようなことをしなくてよい実装になっているからだと思います。(例えば以下のような雰囲気に)
C#
1class List<T> { 2 public void RemoveAll(Predicate<T> predicate) { 3 int curIndex = 0; 4 for (int i = 0; Count; i++) { 5 T element = this[i]; 6 if (!predicate(element)) { 7 if (curIndex < i) { 8 this[curIndex++] = element; 9 } 10 } 11 } 12 RemoveRange(curIndex, Count - curIndex); 13 } 14}
この考えに基づくなら後は「削除すべき要素かどうかを高速に判定する」ということに配慮しHashSetを用いるとよいと思います。(ソートしてバイナリーサーチする方法とHashSetによりサーチする方法を比べれば検索時間はやはりHashSetの方が早いと思います。既にソートされているかどうかや消費メモリー量など条件によっても違うでしょうが...)
以上の考えから、次のように書き換えるとそこそこのスピードになってくれると思います。
C#
1// 質問者さんのコード 2... 3foreach (string deleteStr in deleteList) { 4 mainList.Remove(deleteStr); 5} 6 7// 変更後のコード 8... 9HashSet<string> deleteSet = new HashSet<string>(deleteList); 10mainList.RemoveAll(element => deleteSet.contains(element));
なお、削除すべきリストを作っている部分を最初からHashSetとして作ればスピードとメモリーの消費量は若干よくなると思います。
投稿2017/06/11 04:15
総合スコア18404
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。

0
600万件をデータベースも使わずにソートするとか
ぞっとしない話ですが、もし可能なら…
- (元データ + 削除データ + 削除データ) を作成
- 上記をソート
- 2つ以上同じものがあったら削除
イメージ的にはこんな感じ
sh
1src_file='hoge.txt' # 元データ 2del_file='piyo.txt' # 削除対象 3dst_file='fuga.txt' # 出力ファイル 4 5cat $src_file $del_file $del_file | \ 6sort | \ 7uniq -c | \ 8awk '{if ($1 == 1) print $2}' > $dst_file
投稿2017/06/11 00:49
総合スコア7466
0
前提条件として
0. メモリが十分に備わっている
0. mainListの要素の並び順が意味を持っている
0. 削除キーが重複してない。
とします。
さて、提示されたサンプルコードは削除するために、約600万件ものデータを総なめする必要があるため余り効率が良くないです。
また、削除する際のコストも結構かかるので、一度辞書にしてしまって再度リストにすれば良いのではないでしょうか?
csharp
1 2 3using System; 4using System.Collections.Generic; 5using System.Linq; 6 7namespace ConsoleApp3 8{ 9 class Program 10 { 11 static void Main(string[] args) 12 { 13 // 600万件分のリストを作成 14 List<string> mainList = new List<string>(); 15 for (int i = 0; i < 6000000; i++) 16 { 17 mainList.Add(i.ToString()); 18 } 19 20 // 50万件分のリストを作成 21 List<string> deleteList = new List<string>(); 22 for (int i = 0; i < 500000; i++) 23 { 24 deleteList.Add(i.ToString()); 25 } 26 27 //mainListを辞書化する(元の位置を覚えておく為インデックスを持っておく) 28 var mainDictionary = mainList.Select((s, i) => (value:s, index:i)).ToDictionary(t => t.value, t => t.index); 29 30 //辞書に対して削除を行う。 31 foreach (var deleteStr in deleteList) 32 { 33 mainDictionary.Remove(deleteStr); 34 } 35 36 //mainDictionaryからListを再作成(インデックスを元に元の並びを復元する) 37 mainList = mainDictionary.OrderBy(p => p.Value).Select(p => p.Key).ToList(); 38 39 40 41 } 42 } 43} 44 45
投稿2017/06/10 23:33
総合スコア260
0
main,deleteデータをソートしておく。(順に並んでいれば何もしない)
1.mainを読む
2.deleteを読む
3.main<deleteならmain出力。main読み込み。main終わりならプログラム終了。
main>deleteならdelete読み込み。delete終わりなら残りのmainをすべて出力してプログラム終了。
main=deleteならmain読み込み。main終わりならプログラム終了。
4.3.の繰り返し
これでどうですか?
マッチングというやつです。
メモリーに貯めることもせず、データを比較して新しいマスターを作っていくものです。
投稿2017/06/10 17:24
編集2017/06/10 18:08総合スコア880
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2017/06/11 12:24