在C#中,这个迭代算法有更好的功能版本吗?

时间:2012-06-21 16:27:36

标签: c# functional-programming iteration

我希望找到一种方法来编写具有扩展功能的功能样式。理想情况下,与迭代/循环版本相比,此功能样式将表现良好。我猜是没有办法。可能是因为许多额外的函数调用和堆栈分配等等。

从根本上说,我认为使其麻烦的模式是它既计算用于Predicate的值,又将该计算值再次作为结果集合的一部分。

// This is what is passed to each function.
// Do not assume the array is in order.
var a = (0).To(999999).ToArray().Shuffle();

// Approx times in release mode (on my machine):
// Functional is avg 20ms per call
// Iterative is avg 5ms per call
// Linq is avg 14ms per call

private static List<int> Iterative(int[] a)
{
    var squares = new List<int>(a.Length);

    for (int i = 0; i < a.Length; i++)
    {
        var n = a[i];

        if (n % 2 == 0)
        {
            int square = n * n;

            if (square < 1000000)
            {
                squares.Add(square);
            }
        }
    }

    return squares;
}

private static List<int> Functional(int[] a)
{
    return
    a
        .Where(x => x % 2 == 0 && x * x < 1000000)
        .Select(x => x * x)
        .ToList();
}

private static List<int> Linq(int[] a)
{
    var squares =
        from num in a
        where num % 2 == 0 && num * num < 1000000
        select num * num;

    return squares.ToList();
}

4 个答案:

答案 0 :(得分:7)

康拉德建议的另一种选择。这避免了双重计算,但也避免了在不需要时计算平方:

return a.Where(x => x % 2 == 0)
        .Select(x => x * x)
        .Where(square => square < 1000000)
        .ToList();

就个人而言,直到我在更大的背景下看到重要之前,我才会说出性能差异。

(顺便说一句,我假设这只是一个例子。通常你可能只计算1000000的平方根,然后只需将n与之比较,以减少几毫秒。当然,它确实需要两次比较或Abs操作。)

编辑:请注意,功能更强大的版本将完全避免使用ToList。返回IEnumerable<int>,然后让调用者将其转换为List<T> ,如果他们想要。如果他们不这样做,他们就不会受到打击。如果他们只想要前5个值,他们可以调用Take(5)。根据上下文的不同,懒惰可以在原始版本上取胜。

答案 1 :(得分:2)

只是解决双重计算的问题:

return (from x in a
        let sq = x * x
        where x % 2 == 0 && sq < 1000000
        select sq).ToList();

那就是说,我不确定这会带来多大的性能提升。功能变体实际上是否明显快于迭代变量?该代码为自动优化提供了相当大的潜力。

答案 2 :(得分:1)

某些并行处理怎么样?或者解决方案必须是LINQ(我认为它很慢)。

var squares = new List<int>(a.Length);

Parallel.ForEach(a, n =>
{
  if(n < 1000 && n % 2 == 0) squares.Add(n * n);             
}

Linq版本将是:

return a.AsParallel()
  .Where(n => n < 1000 && n % 2 == 0)  
  .Select(n => n * n)
  .ToList();

答案 3 :(得分:0)

我认为没有一种功能性解决方案可以完全与性能方面的迭代解决方案相媲美。在我的时间(见下文)中,OP的“功能”实现似乎是迭代实现的两倍慢。

像这样的微基准测试容易出现各种问题。处理可变性问题的常用策略是反复调用方法定时并计算每次调用的平均时间 - 如下所示:

// from main
Time(Functional, "Functional", a);    
Time(Linq, "Linq", a);    
Time(Iterative, "Iterative", a);
// ...

static int reps = 1000;
private static List<int> Time(Func<int[],List<int>> func, string name, int[] a)
{
    var sw = System.Diagnostics.Stopwatch.StartNew();
    List<int> ret = null;
    for(int i = 0; i < reps; ++i)
    {
        ret = func(a);
    }
    sw.Stop();
    Console.WriteLine(
        "{0} per call timings - {1} ticks, {2} ms",
        name,
        sw.ElapsedTicks/(double)reps,
        sw.ElapsedMilliseconds/(double)reps);
    return ret;
}

以下是一次会议的时间安排:

Functional per call timings - 46493.541 ticks, 16.945 ms
Linq per call timings - 46526.734 ticks, 16.958 ms
Iterative per call timings - 21971.274 ticks, 8.008 ms

还有许多其他挑战:使用定时器的频闪效果,实时编译器如何以及何时执行其操作,运行其集合的垃圾收集器,运行竞争算法的顺序, cpu的类型,操作系统交换进出的其他进程等等。

我尝试了一点优化。我从测试中删除了正方形(num * num&lt; 1000000) - 将其更改为(num <1000) - 这似乎是安全的,因为输入中没有负数 - 也就是说,我取了两边的平方根不平等。令人惊讶的是,与OP中的方法相比,我获得了不同的结果 - 我的优化输出中只有500个项目,而OP实现中的三个实现中只有241,849个。为什么差异呢?平方的大部分输入溢出32位整数,因此额外的241,349项来自数字,当平方溢出为负数或数字低于100万而仍然通过我们的均匀度测试时。

优化(功能)时间:

Optimized per call timings - 16849.529 ticks, 6.141 ms

这是根据建议改变的功能实现之一。它按预期输出通过标准的500个项目。它看起来很“快”,因为它输出的项目少于迭代解决方案。

我们可以通过在实现周围添加一个选中的块来使原始实现以溢出异常爆炸。这是一个添加到“迭代”方法的已检查块:

private static List<int> Iterative(int[] a)
{
    checked
    {
        var squares = new List<int>(a.Length);

        // rest of method omitted for brevity...

        return squares;
    }
}