【发布时间】:2023-01-13 05:06:35
【问题描述】:
我有一个包含大约 200 万条记录的数据。我试图找到最接近给定时间范围的单个数据。数据列表是有序的,数据由以下类表示:
public class DataPoint
{
public long OpenTimeTs;
}
我已经实施了 3 种方法来完成相同的工作并产生相同的结果。我对为什么其中一种方法执行得更快有一些疑问
方法一
在long 列表中使用二进制搜索
private DataPoint BinaryFindClosest(List<DataPoint> candles, List<long> times, long dateToFindMs)
{
int index = times.BinarySearch(dateToFindMs);
if (index >= 0)
return candles[index];
// If not found, List.BinarySearch returns the complement
// of the index where the element should have been.
index = ~index;
// This date search for is larger than any
if (index == times.Count)
return candles[index - 1];
// The date searched is smaller than any in the list.
if (index == 0)
return candles[0];
if (Math.Abs(dateToFindMs - times[index - 1]) < Math.Abs(dateToFindMs - times[index]))
return candles[index - 1];
else
return candles[index];
}
方法二
与方法 1 几乎相同,除了它使用自定义对象比较器。
private DataPoint BinaryFindClosest2(List<DataPoint> candles, DataPoint toFind)
{
var comparer = Comparer<DataPoint>.Create((x, y) => x.OpenTimeTs > y.OpenTimeTs ? 1 : x.OpenTimeTs < y.OpenTimeTs ? -1 : 0);
int index = candles.BinarySearch(toFind, comparer);
if (index >= 0)
return candles[index];
// If not found, List.BinarySearch returns the complement
// of the index where the element should have been.
index = ~index;
// This date search for is larger than any
if (index == candles.Count)
return candles[index - 1];
// The date searched is smaller than any in the list.
if (index == 0)
return candles[0];
if (Math.Abs(toFind.OpenTimeTs - candles[index - 1].OpenTimeTs) < Math.Abs(toFind.OpenTimeTs - candles[index].OpenTimeTs))
return candles[index - 1];
else
return candles[index];
}
方法三
最后,这是我在其他主题中发现关于 stackoverflow 的 BinarySearch 方法之前一直使用的方法。
private DataPoint FindClosest(List<DataPoint> candles, DataPoint toFind)
{
long timeToFind = toFind.OpenTimeTs;
int smallestDistanceIdx = -1;
long smallestDistance = long.MaxValue;
for (int i = 0; i < candles.Count(); i++)
{
var candle = candles[i];
var distance = Math.Abs(candle.OpenTimeTs - timeToFind);
if (distance <= smallestDistance)
{
smallestDistance = distance;
smallestDistanceIdx = i;
}
else
{
break;
}
}
return candles[smallestDistanceIdx];
}
问题
现在问题来了。运行一些基准测试后,我注意到第二种方法(使用自定义比较器)是其他方法中最快的。
我想知道为什么自定义比较器的方法比 longs 列表中的二进制搜索方法执行得更快。
我正在使用以下代码来测试这些方法:
var candles = AppState.GetLoadSymbolData();
var times = candles.Select(s => s.OpenTimeTs).ToList();
var dateToFindMs = candles[candles.Count / 2].OpenTimeTs;
var candleToFind = new DataPoint() { OpenTimeTs = dateToFindMs };
var numberOfFinds = 100_000;
var sw = Stopwatch.StartNew();
for (int i = 0; i < numberOfFinds; i++)
{
var foundCandle = BinaryFindClosest(candles, times, dateToFindMs);
}
sw.Stop();
var elapsed1 = sw.ElapsedMilliseconds;
sw.Restart();
for (int i = 0; i < numberOfFinds; i++)
{
var foundCandle = BinaryFindClosest2(candles, candleToFind);
}
sw.Stop();
var elapsed2 = sw.ElapsedMilliseconds;
sw.Restart();
for (int i = 0; i < numberOfFinds; i++)
{
var foundCandle = FindClosest(candles, candleToFind);
}
sw.Stop();
var elapsed3 = sw.ElapsedMilliseconds;
Console.WriteLine($"Elapsed 1: {elapsed1} ms");
Console.WriteLine($"Elapsed 2: {elapsed2} ms");
Console.WriteLine($"Elapsed 3: {elapsed3} ms");
在发布模式下,结果如下:
- 经过 1:19 毫秒
- 经过 2:1 毫秒
- 经过 3:60678 毫秒
从逻辑上讲,我会假设比较多头列表应该更快,但事实并非如此。我尝试分析代码,但它只指向 BinarySearch 方法执行缓慢,没有别的。所以必须有一些内部进程会减慢 longs 的速度。
编辑:听从建议后,我使用benchmarkdotnet 实施了适当的基准测试,结果如下
| Method | N | Mean | Error | StdDev | Gen0 | Allocated |
|---|---|---|---|---|---|---|
| BinaryFindClosest | 10000 | 28.31 ns | 0.409 ns | 0.362 ns | - | - |
| BinaryFindClosest2 | 10000 | 75.85 ns | 0.865 ns | 0.722 ns | 0.0014 | 24 B |
| FindClosest | 10000 | 3,363,223.68 ns | 63,300.072 ns | 52,858.427 ns | - | 2 B |
看起来执行方法的顺序确实弄乱了我的初始结果。现在看起来第一种方法工作得更快(而且应该如此)。最慢的当然是我自己实现了。我稍微调整了一下,但它仍然是最慢的方法:
public static DataPoint FindClosest(List<DataPoint> candles, List<long> times, DataPoint toFind)
{
long timeToFind = toFind.OpenTimeTs;
int smallestDistanceIdx = -1;
long smallestDistance = long.MaxValue;
var count = candles.Count();
for (int i = 0; i < count; i++)
{
var diff = times[i] - timeToFind;
var distance = diff < 0 ? -diff : diff;
if (distance < smallestDistance)
{
smallestDistance = distance;
smallestDistanceIdx = i;
}
else
{
break;
}
}
return candles[smallestDistanceIdx];
}
长话短说——使用合适的基准测试工具。
【问题讨论】:
-
请提供minimal reproducible example,包括列表初始化。旁注:通常强烈建议不要自行测量时间,而应使用一些现有的经过验证的时间,例如benchmarkdotnet.org
-
您可能想尝试使用不同的起始值进行搜索。对于中间的值,对于二进制搜索,您可能会直接命中,而您测量的差异只是查找默认比较器而不是使用给定比较器的开销。甚至可能在此比较中使用随机起始值。
-
对不起我的愚蠢问题。如果列表是有序的(因为它应该应用任何 divide et impera 算法)为什么你花时间编写假设列表未排序的第三种方法?
-
+1 到 Alexei 的评论 - 你的时间测量没有考虑到这样一个事实,即 CLR 可以在执行几次后重新编译代码,如果它认为它是一条热路径并且应该优化的话。我怀疑如果您要更改测试顺序,您的时间安排会有所不同。
benchmarkdotnet自动计算 -
@AndrewWilliamson 这是正确的。我改变了顺序,现在方法 2 工作得更快。我会写一些基准并尽快更新帖子。附言我可以保证数据是按
time (milliseconds)升序排列的。所以这很好..
标签: c# binary-search