【问题标题】:Faster alternative to nested loops?嵌套循环的更快替代方案?
【发布时间】:2015-06-29 14:34:11
【问题描述】:

我需要创建一个数字组合列表。数字很​​小,所以我可以使用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 之类的东西,但我不确定如何合并它。

任何建议将不胜感激。或者,也许这是做我想做的最快的方式?

编辑 几个要点(很抱歉我没有把这些放在原始帖子中):

  • 数字和它们的顺序(2、3、4、3、4、3、3 等)非常重要,因此使用 Generating Permutations using LINQ 之类的解决方案无济于事,因为每个 ' 列中的最大值' 不同
  • 我不是数学家,如果我没有正确使用“排列”和“组合”等技术术语,我深表歉意:)
  • 确实需要一次填充所有这些组合 - 我不能只根据索引抓取一个或另一个
  • 使用byte 比使用int 更快,我保证。拥有 67m+ 字节数组而不是 ints 在内存使用方面也好很多
  • 我的最终目标是寻找一种更快的替代嵌套循环的方法。
  • 我考虑过使用并行编程,但由于我想要实现的迭代性质,我找不到成功的方法(即使使用ConcurrentBag)——但我很高兴证明是错误的:)

结论

Caramiriel 提供了一个很好的微优化,可以缩短循环时间,因此我已将该答案标记为正确。 Eric 还提到,预先分配 List 会更快。但是,在这个阶段,嵌套循环似乎实际上是最快的方法(我知道,令人沮丧!)。

如果您想准确地尝试使用StopWatch 进行基准测试,请使用 13 个循环,每个循环最多计数 4 个 - 列表中大约有 67m+ 行。在我的机器上(i5-3320M 2.6GHz)做优化版本大约需要2.2s。

【问题讨论】:

  • 尝试使用 linq,如果你使用的是多核处理器,那么 Parrallel.for
  • 根据我所看到的,这些不是排列,而是几个非常小的(2-4 个元素)集合的组合是正确的,或者您确实想要 的所有/某些排列一套
  • 我假设你已经搜索了 bing.com/search?q=c%23+permutation+enumerable 并且出于某种原因(帖子中未提及)决定反对现有的答案,如 stackoverflow.com/questions/4319049/…... 考虑列出你查看并决定反对的选项问题更好。
  • 如果这与性能有关:您可以预先分配列表(构造函数)并展开一些循环,但我认为仅此而已......除了预先计算和存储这些数字。循环(开销)可能是所有循环中成本最高的,因为主体内部的操作量很少。
  • @benpage:为什么需要预先生成所有组合?为什么不在需要的时候从它的索引中生成一个组合呢?

标签: c# combinations


【解决方案1】:

提醒一下:在开发自己的解决方案时,您可能不需要这种代码。这可以而且应该只在非常特定的情况下使用。可读性通常比速度更重要。

您可以使用结构的属性并提前分配结构。我在下面的示例中截断了一些级别,但我相信您将能够弄清楚具体细节。运行速度比原来快大约 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)的引用。您可以假设每个值在循环内只修改一次,因此我们可以对其进行优化(数据局部性)。

还请阅读 cmets 了解副作用。

编辑了答案以使用T[] 而不是List&lt;T&gt;

【讨论】:

  • 它是一个结构,所以你应该没问题 =) 它们都是独一无二的。它在调用List&lt;T&gt;.Add 方法时被复制。
  • 如果你将容量分配给 List() 会更快
  • 在堆栈上分配太多对象时要注意 stackoverflow 异常。
  • @Andrew 我不明白你的意思。此代码不是递归的,并且堆栈使用量最少。
  • @Andrew:那是内存不足,而不是 stackoverflow。这是因为List&lt;T&gt;.Add() 方法超出了它可以存储的范围。这将使其调整大小(大小翻倍),超过 2GB 的内存。尝试使用 new List(maxPerLevel.Aggregate(1, (x, y) => x*y)) 进行预分配,尽管它已经“随机”需要内存中完整的 2GB 数据块。还要注意 data.ToArray();很昂贵,因为它在那时将项目两次保存在内存中。 [改写]
【解决方案2】:

您正在做的是计数(使用可变基数,但仍在计数)。

由于您使用的是 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]

如您所见,即使您预先计算了整个列表,您也可以轻松避免所有这些循环 :),因为您可以简单地实现一个进位波纹计数器。

计数器是一门经过深入研究的学科,如果您觉得,我强烈建议您搜索一些文献。

【讨论】:

  • 我喜欢你的回答,但说​​所有其他答案都是指数级的说法是不正确的。
  • 与 Caramiriel 的回答相比,这方面的速度如何?
  • “C-kiddy-#”,真的吗?这似乎完全没有必要。
  • 确实如此:Math.DivRem
  • 我认为在某种程度上,优化是一个使用问题。例如,如果每个数组只使用一次,您可以避免密集的内存分配,这是我认为的关键瓶颈。此外,如果你想计算所有的值,你应该利用你做单增量(即+1增量)避免除法的事实。这更多是作为“开箱即用”的答案或原型,我并没有真正尝试加快速度,我只是喜欢这样:)
【解决方案3】:

方法一

如果您打算继续使用List&lt;byte[]&gt;,则可以指定容量,就像这样。

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

方法二

此外,您可以直接使用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 };

这需要 596ms 在我的计算机上完成,这比相关代码(需要 658ms)快 10.4%

方法3

或者,您可以使用以下技术进行适合以稀疏方式访问的低成本初始化。当可能只需要一些元素并且预先确定它们被认为是不必要的时,这尤其有利。此外,在内存不足的情况下处理更大的元素时,此类技术可能成为唯一可行的选择。

在这个实现中,每个元素都在访问时被延迟地、动态地确定。当然,这是以访问期间产生的额外 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)
            };
        }
    }
}

这需要 897ms 在我的计算机上完成(还创建并添加到 Array,如 方法 2),大约是 36.3比相关代码(需要 658 毫秒)慢 %

【讨论】:

  • 您的第二个建议也是显着节省内存消耗。 (但我会注意到它假定列表不应该改变)
  • 我需要一次创建整个列表 - 我无法引用列表中的索引。
  • @Taemyr 谢谢。我会相应地更新以说明这一点。如果实现确实坚持要您预先填充整个列表,那么这第三个选项显然不适合您。
  • @benpage 为什么需要填充列表?
【解决方案4】:

在我的机器上,这会在 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;
}

【讨论】:

  • 这是一个很好的答案!不幸的是,它的运行速度比嵌套循环慢。您有机会使用 TPL 进行编辑吗?
  • 还是有点慢,很遗憾。
  • @benpage 有一种简单的方法可以让它至少快 2 倍。您只需将结果类型更改为 int[,]。这将在一次调用中分配整个数组内存。我不确定这如何满足您的需求(更改返回类型)。
【解决方案5】:
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;
}

【讨论】:

  • 这运行得慢很多:(
【解决方案6】:

List 在内部有一个数组,用于存储它的值,长度固定。当您调用 List.Add 时,它会检查是否有足够的空间。当它无法添加新元素时,它将创建一个更大的新数组,复制所有以前的元素,然后添加新的元素。这需要相当多的周期。

由于您已经知道元素的数量,您可以创建正确大小的列表,这应该会快很多。

另外,不确定您如何访问这些值,但您可以创建一个并将图像保存在代码中(从磁盘加载它可能会比您现在正在做的要慢。您阅读了多少次/给这个东西写信?

【讨论】:

  • 我实际上已经尝试过预先分配一个常规数组,不管你信不信,它更慢。正如我上面所说,这需要即时创建,我不能计算一次就离开它。
  • 真的吗?哇-您正在运行优化吗? (只是问)
  • 啊,这是另一个问题,常规数组 [x,y] 很好用,但数组数组会更快。 stackoverflow.com/questions/597720/… 因为它们是如何在 IL 的底层实现的
【解决方案7】:

这是另一种方式,只需要 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 转换过来的

【讨论】:

    【解决方案8】:

    你所有的数字都是编译时间常数。

    如何将所有循环展开成一个列表(使用您的程序编写代码):

    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#不太熟悉,但似乎有一些序列化对象的方法。如果您只是生成了该 List 并以某种形式将其序列化怎么办?不过,我不确定反序列化是否比创建列表和添加元素更快。

    【讨论】:

    • 序列化是一种非常好的开箱即用方法!
    • 不幸的是列表中的最大值是动态的,我不能静态输入。好主意!
    【解决方案9】:

    您是否需要将结果作为数组数组?在当前设置下,内部数组的长度是固定的,可以用结构替换。这将允许将整个事物保留为一个连续的内存块,并提供对元素的更轻松访问(不确定您以后如何使用此事物)。

    下面的方法要快得多(我的盒子上原来的方法是 41ms 对 1071ms):

    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;
    }
    

    【讨论】:

    • 好主意 - 事实上,这实际上是我在现实世界项目中所做的 - 因为简单,我只是没有将它放在原始解决方案中。我主要是在寻找嵌套循环的更好替代方案。
    【解决方案10】:

    使用Parallel.For() 运行它怎么样? (结构优化感谢 @Caramiriel)。我稍微修改了值(a 是 5 而不是 2),所以我对结果更有信心。

        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 倍(1.718 秒对 0.266 秒)。

    【讨论】:

    • 这几乎可以保证给你虚假分享,并且可能会慢很多倍。
    • 不错 - 不幸的是,它比 for 循环运行 。 FWIW 我用 ALL Parallel.Fors 试了一下,VS 崩溃了!
    • @gjvdkamp 我已经用并行版本更新了我的答案,我相信它可以消除虚假共享问题。
    【解决方案11】:

    您的某些数字完全适合整数位数,因此您可以将它们与上层数字“打包”:

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

    当然,这会降低代码的可读性,但您节省了一个循环。每次其中一个数字是 2 的幂(在您的情况下是 7 次)时,都可以这样做。

    【讨论】:

    • 我想了解更多关于这个答案的信息 - 你能详细说明一下吗?
    • 很抱歉回答迟了。 m 从 0 到 3,二进制表示 00 到 11,l 从 0 到 2,表示 00 到 10,所以如果你分开打印它们,这将是:00 00 00 01 00 10 00 11 01 00 .. . 10 11 您可以将它们合并为单个 4 位,从 0000 到 1011,并使用掩码选择适当的位 lm & 3 使 lm 和 (11)b 之间的二元和 lm&12 与 lm 相同和 (1100)b 然后我们移动两位以获得“实数”。顺便说一句,刚刚意识到在这种情况下执行 lm >> 2 就足够了。
    【解决方案12】:

    这是另一种解决方案。在 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();
    }
    

    【讨论】:

      猜你喜欢
      • 2023-01-17
      • 1970-01-01
      • 2018-04-15
      • 1970-01-01
      • 2021-05-25
      • 2013-08-23
      • 1970-01-01
      • 1970-01-01
      • 2017-09-03
      相关资源
      最近更新 更多