有没有办法在C#中调用写通用程序和算法,而避免动态解决方案的开销?
考虑一个简单的例子:
static void QuickSort<T>(T[] arr, int left, int right, Comparison<T> compare)
{
do
{
int i = left;
int j = right;
var x = arr[i + ((j - i) >> 1)];
do
{
while (i < arr.Length && compare(x, arr[i]) > 0) i++;
while (j >= 0 && compare(x, arr[j]) < 0) j--;
if (i > j) { break; }
if (i < j)
{
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
i++;
j--;
} while (i <= j);
if (j - left <= right - i)
{
if (left < j) QuickSort(arr, left, j, compare);
left = i;
}
else
{
if (i < right) QuickSort(arr, i, right, compare);
right = j;
}
} while (left < right);
}
你可以称之为:
QuickSort(buffer, 0, buffer.Length - 1, (a, b) => a.CompareTo(b))
虽然看似高效,但这个看起来很好看的示例为每次比较执行间接(即虚拟)调用。
显然,处理器无法优化间接呼叫,因此它们的性能很差。在我的电脑上,这意味着性能下降了25%,从大约3,600项/ ms到2,700项/ ms。
有没有办法在编写通用代码时避免这种间接调用?
无论我对代表,DynamicMethod
等做多少玩杂耍,似乎总是在库代码和用户代码之间进行间接调用,这显然会影响性能负
答案 0 :(得分:5)
如果比较项目的答案是否定的:你不能把>
放在x
和arr[j]
之间,并期望编译器弄清楚你的意思是将其内置>
运算符应用于T
类型的对象。
但是,您的解决方案可能会稍微优化一下,因为您需要为间接付费两次。由于您已经宣布T
已IComparable<T>
,因此可以删除comparator
参数,并在不经过lambda的情况下调用x.CompareTo(arr[j])
。这将减少第二个间接的开销。当然,它不会让你自定义你比较你的项目的方式,但这是一个支付CPU周期灵活性的例行情况。
答案 1 :(得分:2)
您的结果将在很大程度上取决于T
的类型以及比较的成本。
我创建了QuickSort
方法的自定义版本:一个期望数组为int
,另一个期望数组为string
。修改仅限于删除比较参数并更改分区程序中的两个比较。我将它们修改为反向排序,如下所示:
while (i < arr.Length && arr[i].CompareTo(x) > 0) i++;
while (j >= 0 && arr[j].CompareTo(x) < 0) j--;
然后,我使用1000万个项目的数组,针对您的泛型方法测试了这些方法。我的结果:
Int: Generic QuickSort - 2,190 ms
Int: Custom QuickSort - 1,252 ms
String: Generic QuickSort - 32,902 ms
String: Custom QuickSort - 31,634 ms
我的结论是,如果比较非常便宜(与int
和其他原生类型相比),那么您将注意到性能的巨大差异。如果比较成本很高(比较字符串相当昂贵),那么虚拟调用开销会在比较成本中丢失。
我知道这不能解决你的问题;我不认为有一个。灵活性通常需要付出代价。
构建基类库的人理解这一点。例如,他们为使用默认IComparer
的基本类型创建了特殊情况。比较以这两种方式调用Array.Sort
时的运行时间差异(使用int[10000000]
):
Array.Sort(a); // 845 ms
Array.Sort(a, (a, b) => a.CompareTo(b)); // 2,339 ms
事实证明,Array.Sort
对使用默认IComparer
的基本类型进行了内置优化。有关详细信息,请参阅Of Comparison and IComparer。
答案 2 :(得分:1)
我认为dasblinkenlight是对的,但我冒昧地猜测为什么:
当您将Comparer传递给QuickSort方法时,Framework正在创建System.Comparison
委托的通用实现(例如System.Comparison
1`)。对任何泛型委托的调用都是虚拟的,这是有道理的 - 编译器如何能够静态生成对仅在运行时创建的泛型类型的方法的调用?
Reed Copsey在这里更深入地描述了这一点:http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/b94c7506-e21f-43b1-be9a-bf88f8f72f36
我能得到的最接近的工厂模式是返回已知类型的非虚拟调用:
using System;
using System.Diagnostics;
using System.Linq;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
const int size = 50000;
var ra = RandomArray(size);
var buffer = Enumerable.Range(0, size).OrderBy(i => ra[i]).ToArray();
Debug.WriteLine(String.Join(",", buffer));
new IntSorter().QuickSort(buffer);
Debug.WriteLine(String.Join(",", buffer));
}
public IQuickSorter<T> GetSorter<T>() where T : IComparable<T>
{
if (typeof(T).Equals(typeof(Int32)))
return (IQuickSorter<T>) new IntSorter();
return new GenericSorter<T>();
}
public static Int32[] RandomArray(Int32 length)
{
var r = new Random();
return Enumerable.Range(0, length).Select(i => r.Next(0, length + 1)).ToArray();
}
}
public class IntSorter : IQuickSorter<int>
{
public void QuickSort(int[] arr)
{
QuickSortInner(arr, 0, arr.Length-1);
}
public void QuickSortInner(int[] arr, int left, int right)
{
do
{
int i = left;
int j = right;
var x = arr[i + ((j - i) >> 1)];
do
{
while (i < arr.Length && x.CompareTo(arr[i]) > 0) i++;
while (j >= 0 && x.CompareTo(arr[j]) < 0) j--;
if (i > j)
{
break;
}
if (i < j)
{
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
i++;
j--;
} while (i <= j);
if (j - left <= right - i)
{
if (left < j) QuickSortInner(arr, left, j);
left = i;
}
else
{
if (i < right) QuickSortInner(arr, i, right);
right = j;
}
} while (left < right);
}
}
public class GenericSorter<T> : IQuickSorter<T> where T : IComparable<T>
{
public void QuickSort(T[] arr)
{
QuickSortInner(arr, 0, arr.Length - 1);
}
public void QuickSortInner(T[] arr, int left, int right)
{
do
{
int i = left;
int j = right;
var x = arr[i + ((j - i) >> 1)];
do
{
while (i < arr.Length && x.CompareTo(arr[i]) > 0) i++;
while (j >= 0 && x.CompareTo(arr[j]) < 0) j--;
if (i > j) { break; }
if (i < j)
{
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
i++;
j--;
} while (i <= j);
if (j - left <= right - i)
{
if (left < j) QuickSortInner(arr, left, j);
left = i;
}
else
{
if (i < right) QuickSortInner(arr, i, right);
right = j;
}
} while (left < right);
}
}
public interface IQuickSorter<in T>
{
void QuickSort(T[] arr);
}
}
答案 3 :(得分:0)
将quicksort方法放在抽象类型中,完全摆脱委托的使用。
首先,创建抽象类型。请注意新的抽象“比较”方法,以及QuickSort方法上缺少委托:
public abstract class QuickSort<T>
{
protected static abstract int compare(T x, T y);
public static void QuickSort(T[] arr, int left, int right)
{
do
{
int i = left;
int j = right;
var x = arr[i + ((j - i) >> 1)];
do
{
while (i 0) i++;
while (j >= 0 && compare(x, arr[j]) < 0) j--;
if (i > j) { break; }
if (i < j)
{
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
i++;
j--;
} while (i <= j);
if (j - left <= right - i)
{
if (left < j) QuickSort(arr, left, j);
left = i;
}
else
{
if (i < right) QuickSort(arr, i, right);
right = j;
}
} while (left < right);
}
}
接下来,创建一个继承自QuickSort的类并实现compare方法。我们将使用int作为示例:
public class QuickSortInt : QuickSort<int>
{
protected static override int compare(int x, int y)
{
if (x < y) return -1;
if (x > y) return 1;
return 0;
}
}
答案 4 :(得分:0)
示例:
static void QuickSort<TCmp, T>(T[] arr, int left, int right, TCmp cmp)
where TCmp : IComparer<T>
{
do
{
int i = left;
int j = right;
var x = arr[i + ((j - i) >> 1)];
do
{
while (i < arr.Length && cmp.Compare(x, arr[i]) > 0) i++;
while (j >= 0 && cmp.Compare(x, arr[j]) < 0) j--;
if (i > j) { break; }
if (i < j)
{
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
} while (++i <= --j);
if (j - left <= right - i)
{
if (left < j) QuickSort<TCmp, T>(arr, left, j, cmp);
left = i;
}
else
{
if (i < right) QuickSort<TCmp, T>(arr, i, right, cmp);
right = j;
}
} while (left < right);
}
在我的计算机上,当我使用
比较旧版本时QuickSort(copy1, 0, copy1.Length - 1, (x, y) => x.CompareTo(y));
使用
的新版本QuickSort(copy1, 0, copy1.Length - 1, comparer);
我的速度提升了很多(~30%)。