簡體   English   中英

有比較器的二分搜索比沒有比較器快

[英]Binary search with comparer is faster than without

我有一個包含大約 200 萬條記錄的數據。 我試圖找到最接近給定時間范圍的單個數據。 數據列表是有序的,數據由以下class表示:

    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 幾乎相同,除了它使用自定義 object 比較器。

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

問題

現在問題來了。 運行一些基准測試后,我注意到第二種方法(使用自定義比較器)是其他方法中最快的。

我想知道為什么使用自定義比較器的方法比在long列表中進行二進制搜索的方法執行得更快。

我正在使用以下代碼來測試這些方法:

            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方法執行緩慢,沒有別的。所以必須有一些內部進程會減慢long s 的速度。

編輯:遵循建議后,我使用benchmarkdo.net實施了適當的基准測試,結果如下

方法 意思 錯誤 標准偏差 Gen0 已分配
BinaryFindClosest 10000 28.31 納秒 0.409納秒 0.362 納秒 - -
BinaryFindClosest2 10000 75.85 納秒 0.865 納秒 0.722 納秒 0.0014 24乙
查找最近的 10000 3,363,223.68 納秒 63,300.072 納秒 52,858.427 納秒 - 2乙

看起來執行方法的順序確實弄亂了我的初始結果。 現在看起來第一種方法工作得更快(而且應該如此)。 最慢的當然是我自己實現了。 我稍微調整了一下,但它仍然是最慢的方法:

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

長話短說——使用合適的基准測試工具。

請查看方法 1 和 2 生成的 IL。 這可能是一個無效的測試。 它們應該是幾乎相同的機器碼。

第一:我看不出你在哪里保證訂購。 但假設它以某種方式在那里。 二分搜索將在幾乎 20 到 25 步內找到最隱藏的數字 (log2(2.000.000))。 這個測試聞起來很奇怪。

第二: BinaryFindClosestCandle(candles, times, dateToFindMs)的定義在哪里? 為什么它同時收到 class 實例和long列表? 為什么不返回在長列表上應用二分搜索的索引,並用它來索引原始蠟燭列表? (如果您使用 select 創建long列表,列表中的 1:1 關系將保留)

第三:您使用的數據是 class,因此所有元素都在堆上。 您在 method2 中裝箱了一個包含 200 萬個長數字的數組。 這幾乎是一種犯罪。 從堆中引用數據將比比較本身花費更多。 我仍然認為列表沒有排序。

創建一個交換列表以應用 seach 算法,就像您對times所做的那樣,但是將其轉換為帶有.ToArray()的數組並將其放在堆棧上。 我認為市場上沒有比long valueTypes 的默認比較器更好的了。

編輯解決方案提示:根據您在一次查找最小值之前執行的插入次數,我將 go 用於以下內容:

if (insertions/lookups > 300.000)
{
    a. store the index of the minimum (and the minimum value) apart in a dedicated field, I would store also a flag for IsUpdated to get false at the first deletion from the list.
    b. spawn a parallel thread to refresh that index and the minumum value at every now an then (depending on how often you do the lookups) if the IsUpdated is false, or lazily when you start a lookup with a IsUpdated = false.
}
else
{
    use a dictionary with the long as a key ( I suppose that two entities with the same long value are likely to be considered equal).
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM