web-dev-qa-db-ja.com

ネストされたループのより高速な代替手段?

数字の組み合わせのリストを作成する必要があります。数値は非常に小さいため、byteではなくintを使用できます。ただし、可能なすべての組み合わせを取得するには、多くのネストされたループが必要です。私が望んでいることを行うためのより効率的な方法があるかどうか疑問に思っています。これまでのコードは次のとおりです。

var data = new List<byte[]>();
for (byte a = 0; a < 2; a++)
for (byte b = 0; b < 3; b++)
for (byte c = 0; c < 4; c++)
for (byte d = 0; d < 3; d++)
for (byte e = 0; e < 4; e++)
for (byte f = 0; f < 3; f++)
for (byte g = 0; g < 3; g++)
for (byte h = 0; h < 4; h++)
for (byte i = 0; i < 2; i++)
for (byte j = 0; j < 4; j++)
for (byte k = 0; k < 4; k++)
for (byte l = 0; l < 3; l++)
for (byte m = 0; m < 4; m++)
{
    data.Add(new [] {a, b, c, d, e, f, g, h, i, j, k, l, m});
}

BitArrayのようなものを使用することを検討していましたが、どのように組み込むことができるかわかりません。

推奨事項は大歓迎です。あるいは、おそらくこれが私がしたいことをする最も速い方法ですか?

[〜#〜] edit [〜#〜]いくつかの簡単なポイント(および元の投稿にこれらを入れなかった謝罪):

  • それらの数と順序(2、3、4、3、4、3、3など)は非常に重要であるため、 LINQを使用して順列を生成する などのソリューションを使用しても役に立たない各「列」の最大値が異なる
  • 私は数学者ではないので、「順列」や「組み合わせ」などの専門用語を正しく使用していない場合は謝罪します:)
  • Idoこれらの組み合わせをすべて一度に入力する必要があります-インデックスに基づいていずれかを取得することはできません
  • byteを使用することは、intを使用するよりも高速です。Iguaranteeit。また、メモリ使用量では、intではなく67m +バイトの配列を使用する方がはるかに優れています。
  • ここでの私の最終的な目標は、ネストされたループのより高速な代替手段を探すことです。
  • 並列プログラミングを使用することを検討しましたが、達成しようとしていることの反復的な性質のため、それを成功させる方法を見つけることができませんでした(ConcurrentBagでも)-しかし、私は満足しています間違っていることが判明した:)

[〜#〜]結論[〜#〜]

Caramirielは、ループから少し時間を削る優れたマイクロ最適化を提供しているので、その答えを正しいものとしてマークしました。エリックはまた、リストを事前に割り当てる方が速いと述べました。しかし、この段階では、ネストされたループが実際にこれを行うための最速の方法であると思われます(気のめいるようです!)。

StopWatchでベンチマークしようとしていたものを正確に試したい場合は、各ループで最大4つカウントする13のループを使用します。これにより、リストに約67m行以上が作成されます。私のマシン(i5-3320M 2.6GHz)では、最適化されたバージョンを実行するには約2.2秒かかります。

85
benpage

構造体のプロパティを使用して、構造体を事前に割り当てることができます。以下のサンプルではいくつかのレベルを切り取りましたが、詳細を理解できると確信しています。オリジナルの約5〜6倍高速で実行されます(リリースモード)。

ブロック:

struct ByteBlock
{
    public byte A;
    public byte B;
    public byte C;
    public byte D;
    public byte E;
}

ループ:

var data = new ByteBlock[2*3*4*3*4];
var counter = 0;

var bytes = new ByteBlock();

for (byte a = 0; a < 2; a++)
{
    bytes.A = a;
    for (byte b = 0; b < 3; b++)
    {
        bytes.B = b;
        for (byte c = 0; c < 4; c++)
        {
            bytes.C = c;
            for (byte d = 0; d < 3; d++)
            {
                bytes.D = d;
                for (byte e = 0; e < 4; e++)
                {
                    bytes.E = e;
                    data[counter++] = bytes;
                }
            }
        }
    }
}

リストに追加するたびに新しいリストを割り当てることがないため、高速です。また、このリストを作成しているため、他のすべての値(a、b、c、d、e)への参照が必要です。各値はループ内で1回だけ変更されると想定できるため、最適化することができます(データの局所性)。

副作用のコメントも読んでください。

T[]の代わりにList<T>を使用するように回答を編集しました。

60
Caramiriel

あなたがしているのは、カウントです(基数は可変ですが、まだカウントしています)。

C#を使用しているので、コードを最適化する本当にできる便利なメモリレイアウトとデータ構造を使用したくないと思います。

したがって、ここでは異なるケースを投稿していますが、これはあなたのケースに合わないかもしれませんが、注意する価値があります:実際にリストに疎な方法でアクセスする場合、ここでは線形時間でi番目の要素を計算できるクラス(むしろ他の答えとして指数関数的よりも)

class Counter
{
    public int[] Radices;

    public int[] this[int n]
    {
        get 
        { 
            int[] v = new int[Radices.Length];
            int i = Radices.Length - 1;

            while (n != 0 && i >= 0)
            {
                //Hope C# has an IL-opcode for div-and-reminder like x86 do
                v[i] = n % Radices[i];
                n /= Radices[i--];
            }
            return v;
        }
    }
}

この方法でこのクラスを使用できます

Counter c = new Counter();
c.Radices = new int[] { 2,3,4,3,4,3,3,4,2,4,4,3,4};

c[i]はリストと同じです。名前はll[i]

ご覧のとおり、単純にキャリーリップルカウンターを実装できるため、すべてのリスト全体を事前に計算する場合でも、これらすべてのループを簡単に回避できます。

カウンターは非常に研究された主題である、あなたが感じるならば、私はいくつかの文献を捜すことを強く勧めます。

33
user781847

方法1

高速化する1つの方法は、List<byte[]>、 このような。

var data = new List<byte[]>(2 * 3 * 4 * 3 * 4 * 3 * 3 * 4 * 2 * 4 * 4 * 3 * 4);

方法2

さらに、System.Array直接アクセスして、より高速にアクセスします。すべての要素がメモリ内に物理的に事前に実装されているとの質問がある場合は、このアプローチをお勧めします。

var data = new byte[2 * 3 * 4 * 3 * 4 * 3 * 3 * 4 * 2 * 4 * 4 * 3 * 4][];
int counter = 0;

for (byte a = 0; a < 2; a++)
    for (byte b = 0; b < 3; b++)
        for (byte c = 0; c < 4; c++)
            for (byte d = 0; d < 3; d++)
                for (byte e = 0; e < 4; e++)
                    for (byte f = 0; f < 3; f++)
                        for (byte g = 0; g < 3; g++)
                            for (byte h = 0; h < 4; h++)
                                for (byte i = 0; i < 2; i++)
                                    for (byte j = 0; j < 4; j++)
                                        for (byte k = 0; k < 4; k++)
                                            for (byte l = 0; l < 3; l++)
                                                for (byte m = 0; m < 4; m++)
                                                    data[counter++] = new[] { a, b, c, d, e, f, g, h, i, j, k, l, m };

これは、コンピューターで完了するのに596 msかかります。これは、問題のコード(658msかかります)よりも約10.4%高速です。

方法

または、スパース方式でのアクセスに適した低コストの初期化に次の手法を使用できます。これは、一部の要素のみが必要であり、それらをすべて事前に決定する必要がないと考えられる場合に特に有利です。さらに、このような手法は、メモリが不足しているときにより大きな要素を操作する場合に唯一の実行可能なオプションになる可能性があります。

この実装では、すべての要素はアクセス時にその場で遅延して決定されます。当然、これにはアクセス中に発生する追加のCPUのコストがかかります。

class HypotheticalBytes
{
    private readonly int _c1, _c2, _c3, _c4, _c5, _c6, _c7, _c8, _c9, _c10, _c11, _c12;
    private readonly int _t0, _t1, _t2, _t3, _t4, _t5, _t6, _t7, _t8, _t9, _t10, _t11;

    public int Count
    {
        get { return _t0; }
    }

    public HypotheticalBytes(
        int c0, int c1, int c2, int c3, int c4, int c5, int c6, int c7, int c8, int c9, int c10, int c11, int c12)
    {
        _c1 = c1;
        _c2 = c2;
        _c3 = c3;
        _c4 = c4;
        _c5 = c5;
        _c6 = c6;
        _c7 = c7;
        _c8 = c8;
        _c9 = c9;
        _c10 = c10;
        _c11 = c11;
        _c12 = c12;
        _t11 = _c12 * c11;
        _t10 = _t11 * c10;
        _t9 = _t10 * c9;
        _t8 = _t9 * c8;
        _t7 = _t8 * c7;
        _t6 = _t7 * c6;
        _t5 = _t6 * c5;
        _t4 = _t5 * c4;
        _t3 = _t4 * c3;
        _t2 = _t3 * c2;
        _t1 = _t2 * c1;
        _t0 = _t1 * c0;
    }

    public byte[] this[int index]
    {
        get
        {
            return new[]
            {
                (byte)(index / _t1),
                (byte)((index / _t2) % _c1),
                (byte)((index / _t3) % _c2),
                (byte)((index / _t4) % _c3),
                (byte)((index / _t5) % _c4),
                (byte)((index / _t6) % _c5),
                (byte)((index / _t7) % _c6),
                (byte)((index / _t8) % _c7),
                (byte)((index / _t9) % _c8),
                (byte)((index / _t10) % _c9),
                (byte)((index / _t11) % _c10),
                (byte)((index / _c12) % _c11),
                (byte)(index % _c12)
            };
        }
    }
}

これは、コンピューターで完了するのに897 msかかります(方法2のようにArrayを作成して追加する)。 6.3%遅い問題のコードよりも(658msかかります)。

14
Biscuits

私のマシンでは、これにより、222ミリ秒と760ミリ秒(13のforループ)の組み合わせが生成されます。

private static byte[,] GenerateCombinations(byte[] maxNumberPerLevel)
{
    var levels = maxNumberPerLevel.Length;

    var periodsPerLevel = new int[levels];
    var totalItems = 1;
    for (var i = 0; i < levels; i++)
    {
        periodsPerLevel[i] = totalItems;
        totalItems *= maxNumberPerLevel[i];
    }

    var results = new byte[totalItems, levels];

    Parallel.For(0, levels, level =>
    {
        var periodPerLevel = periodsPerLevel[level];
        var maxPerLevel = maxNumberPerLevel[level];
        for (var i = 0; i < totalItems; i++)
            results[i, level] = (byte)(i / periodPerLevel % maxPerLevel);
    });

    return results;
}
13
Andrei Tătar
var numbers = new[] { 2, 3, 4, 3, 4, 3, 3, 4, 2, 4, 4, 3, 4 };
var result = (numbers.Select(i => Enumerable.Range(0, i))).CartesianProduct();

http://ericlippert.com/2010/06/28/computing-a-cartesian-product-with-linq/ で拡張メソッドを使用する

public static IEnumerable<IEnumerable<T>> CartesianProduct<T>(this IEnumerable<IEnumerable<T>> sequences)
{
    // base case: 
    IEnumerable<IEnumerable<T>> result =
        new[] { Enumerable.Empty<T>() };
    foreach (var sequence in sequences)
    {
        // don't close over the loop variable (fixed in C# 5 BTW)
        var s = sequence;
        // recursive case: use SelectMany to build 
        // the new product out of the old one 
        result =
            from seq in result
            from item in s
            select seq.Concat(new[] { item });
    }
    return result;
}
8
Eric

リストには、固定長の値を格納する配列が内部にあります。 List.Addを呼び出すと、十分なスペースがあるかどうかがチェックされます。新しい要素を追加できない場合は、より大きなサイズの新しい配列を作成し、以前の要素をすべてコピーしてから、新しい要素を追加します。これにはかなりのサイクルがかかります。

すでに要素の数がわかっているので、正しいサイズのリストを作成できます。これは、すでにかなり高速になっているはずです。

また、値にアクセスする方法がわかりませんが、このものを作成してコードに画像を保存することができます(ディスクからの読み込みは、おそらく今実行しているよりも遅くなります。事?

8
gjvdkamp

ループが2つだけ必要な別の方法を次に示します。アイデアは、最初の要素を増やし、その数が超えた場合、次の要素を増やすことです。

データを表示する代わりに、currentValues.Cloneを使用して、そのクローンバージョンをリストに追加できます。私にとっては、これはあなたのバージョンよりも速く走りました。

byte[] maxValues = {2, 3, 4};
byte[] currentValues = {0, 0, 0};

do {
    Console.WriteLine("{0}, {1}, {2}", currentValues[0], currentValues[1], currentValues[2]);

    currentValues[0] += 1;

    for (int i = 0; i <= maxValues.Count - 2; i++) {
        if (currentValues[i] < maxValues[i]) {
            break;
        }

        currentValues[i] = 0;
        currentValues[i + 1] += 1;
    }

// Stop the whole thing if the last number is over
// } while (currentValues[currentValues.Length-1] < maxValues[maxValues.Length-1]);
} while (currentValues.Last() < maxValues.Last());
  • このコードが機能することを願って、vbから変換しました
5
the_lotus

すべての数値はコンパイル時定数です。

すべてのループをリストに展開するのはどうですか(プログラムを使用してコードを書く):

data.Add(new [] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
data.Add(new [] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
etc.

これにより、少なくともforループのオーバーヘッド(ある場合)を取り除くことができます。

私はC#にあまり詳しくありませんが、オブジェクトをシリアル化するいくつかの方法があるようです。そのリストを生成し、何らかの形式でシリアル化した場合はどうなりますか?ただし、逆シリアル化がリストの作成と要素の追加よりも速いかどうかはわかりません。

3
null

結果を配列の配列にする必要がありますか?現在の設定では、内部配列の長さが固定されており、構造体に置き換えることができます。これにより、モノ全体が1つの連続したメモリブロックとして予約され、要素へのアクセスが容易になります(後でこのモノをどのように使用するかはわかりません)。

以下のアプローチははるかに高速です(私のボックスのオリジナルの1071msに対して41ms):

struct element {
    public byte a;
    public byte b;
    public byte c;
    public byte d;
    public byte e;
    public byte f;
    public byte g;
    public byte h;
    public byte i;
    public byte j;
    public byte k;
    public byte l;
    public byte m;
}

element[] WithStruct() {
    var t = new element[3981312];
    int z = 0;
    for (byte a = 0; a < 2; a++)
    for (byte b = 0; b < 3; b++)
    for (byte c = 0; c < 4; c++)
    for (byte d = 0; d < 3; d++)
    for (byte e = 0; e < 4; e++)
    for (byte f = 0; f < 3; f++)
    for (byte g = 0; g < 3; g++)
    for (byte h = 0; h < 4; h++)
    for (byte i = 0; i < 2; i++)
    for (byte j = 0; j < 4; j++)
    for (byte k = 0; k < 4; k++)
    for (byte l = 0; l < 3; l++)
    for (byte m = 0; m < 4; m++)
    {
        t[z].a = a;
        t[z].b = b;
        t[z].c = c;
        t[z].d = d;
        t[z].e = e;
        t[z].f = f;
        t[z].g = g;
        t[z].h = h;
        t[z].i = i;
        t[z].j = j;
        t[z].k = k;
        t[z].l = l;
        t[z].m = m;
        z++;
    }
    return t;
}
2
gjvdkamp

Parallel.For()を使用して実行するのはどうですか? (@ Caramirielへの構造最適化の称賛)。値を少し変更し(aは2ではなく5)、結果に自信を付けました。

_    var data = new ConcurrentStack<List<Bytes>>();
    var sw = new Stopwatch();

    sw.Start();

    Parallel.For(0, 5, () => new List<Bytes>(3*4*3*4*3*3*4*2*4*4*3*4),
      (a, loop, localList) => {
        var bytes = new Bytes();
        bytes.A = (byte) a;
        for (byte b = 0; b < 3; b++) {
          bytes.B = b;
          for (byte c = 0; c < 4; c++) {
            bytes.C = c; 
            for (byte d = 0; d < 3; d++) {
              bytes.D = d; 
              for (byte e = 0; e < 4; e++) {
                bytes.E = e; 
                for (byte f = 0; f < 3; f++) {
                  bytes.F = f; 
                  for (byte g = 0; g < 3; g++) {
                    bytes.G = g; 
                    for (byte h = 0; h < 4; h++) {
                      bytes.H = h; 
                      for (byte i = 0; i < 2; i++) {
                        bytes.I = i; 
                        for (byte j = 0; j < 4; j++) {
                          bytes.J = j; 
                          for (byte k = 0; k < 4; k++) {
                            bytes.K = k; 
                            for (byte l = 0; l < 3; l++) {
                              bytes.L = l;
                              for (byte m = 0; m < 4; m++) {
                                bytes.M = m;
                                localList.Add(bytes);
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }


        return localList;
      }, x => {
        data.Push(x);
    });

    var joinedData = _join(data);
_

_join()はプライベートメソッドで、次のように定義されます:

_private static IList<Bytes> _join(IEnumerable<IList<Bytes>> data) {
  var value = new List<Bytes>();
  foreach (var d in data) {
    value.AddRange(d);
  }
  return value;
}
_

私のシステムでは、このバージョンは約6倍高速に実行されます(0.266秒に対して1.718秒)。

1
jdphenix

別のソリューションがあります。 VSの外では、437.5ミリ秒という高速で実行されます。これは、元のコード(私のコンピューターでは593.7)よりも26%高速です。

static List<byte[]> Combinations(byte[] maxs)
{
  int length = maxs.Length;
  int count = 1; // 3981312;
  Array.ForEach(maxs, m => count *= m);
  byte[][] data = new byte[count][];
  byte[] counters = new byte[length];

  for (int r = 0; r < count; r++)
  {
    byte[] row = new byte[length];
    for (int c = 0; c < length; c++)
      row[c] = counters[c];
    data[r] = row;

    for (int i = length - 1; i >= 0; i--)
    {
      counters[i]++;
      if (counters[i] == maxs[i])
        counters[i] = 0;
      else
        break;
    }
  }

  return data.ToList();
}
0
Henrik

一部の数値は完全に整数ビットのビットに収まるため、上位レベルの数値で「パック」できます。

for (byte lm = 0; lm < 12; lm++)
{
    ...
    t[z].l = (lm&12)>>2;
    t[z].m = lm&3;
    ...
}

もちろん、これによりコードは読みにくくなりますが、1つのループを保存しました。これは、数値の1つが2の累乗になるたびに実行できます。これは、あなたのケースでは7回です。

0
Fabien Dupont