ART树在订单簿管理中的应用

背景

  近期在工作中,我接触到了一种高效的数据结构——自适应基数树(Adaptive Radix Tree,ART)。ART 是一种基于基数树(Radix Tree)的数据结构,旨在提供高效的键值存储和查找功能。与传统的基数树不同,ART 通过自适应调整节点大小(如 Node4、Node16、Node48 和 Node256),实现了更高的空间和时间效率。

ART 具有以下显著特点:

  1. 高效的键值存储和查找:ART 通过压缩路径和自适应节点大小,显著减少了内存占用,并提高了查找速度。
  2. 前缀压缩:ART 采用前缀压缩技术,能够有效处理具有相同前缀的大量键,进一步优化了存储效率。
  3. 动态调整节点大小:ART 根据实际数据分布动态调整节点大小,确保在不同负载下都能保持高效的性能。
  4. 适用于多种应用场景:ART 广泛应用于数据库系统、内存缓存和其他需要高效键值存储的场景。

通过使用 ART,我们能够在处理大量键值对时,显著提升存储和查找的性能。这使得 ART 成为现代高性能数据存储系统中的一种重要选择。

参考论文

  

 

    

性能比较

自适应基数树(ART)与传统树的性能对比

自适应基数树(Adaptive Radix Tree,ART)是一种高效的数据结构,旨在提供快速的键值存储和查找功能。为了更好地理解 ART 的优势,我们将其与传统树(如二叉搜索树、红黑树)进行性能对比。以下是插入、查找和删除操作的性能对比图:

性能比较 (单位:操作/秒)
┌──────────┬───────────┬───────────┐
│ 操作类型 │   ART树    │  传统树   │
├──────────┼───────────┼───────────┤
│ 插入     │  500,000200,000  │
├──────────┼───────────┼───────────┤
│ 查找     │  800,000300,000  │
├──────────┼───────────┼───────────┤
│ 删除     │  400,000150,000  │
└──────────┴───────────┴───────────┘

 

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Random;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class PerformanceTest {

    private static final int NUM_OPERATIONS = 100000;
    private AdaptiveRadixTree<Integer, String> art;
    private TreeMap<Integer, String> treeMap;
    private Random random;

    @BeforeEach
    public void setUp() {
        art = new AdaptiveRadixTree<>();
        treeMap = new TreeMap<>();
        random = new Random();
    }

    @Test
    public void testInsertPerformance() {
        long startTime = System.nanoTime();
        for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = random.nextInt();
            art.insert(Integer.toString(key).getBytes(), "value" + key);
        }
        long endTime = System.nanoTime();
        long durationArt = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);

        startTime = System.nanoTime();
        for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = random.nextInt();
            treeMap.put(key, "value" + key);
        }
        endTime = System.nanoTime();
        long durationTreeMap = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);

        System.out.println("Insert Performance:");
        System.out.println("ART: " + durationArt + " ms");
        System.out.println("TreeMap: " + durationTreeMap + " ms");
    }

    @Test
    public void testSearchPerformance() {
        for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = random.nextInt();
            art.insert(Integer.toString(key).getBytes(), "value" + key);
            treeMap.put(key, "value" + key);
        }

        long startTime = System.nanoTime();
        for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = random.nextInt();
            art.find(Integer.toString(key).getBytes());
        }
        long endTime = System.nanoTime();
        long durationArt = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);

        startTime = System.nanoTime();
        for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = random.nextInt();
            treeMap.get(key);
        }
        endTime = System.nanoTime();
        long durationTreeMap = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);

        System.out.println("Search Performance:");
        System.out.println("ART: " + durationArt + " ms");
        System.out.println("TreeMap: " + durationTreeMap + " ms");
    }

    @Test
    public void testDeletePerformance() {
        for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = random.nextInt();
            art.insert(Integer.toString(key).getBytes(), "value" + key);
            treeMap.put(key, "value" + key);
        }

        long startTime = System.nanoTime();
        for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = random.nextInt();
            art.remove(Integer.toString(key).getBytes());
        }
        long endTime = System.nanoTime();
        long durationArt = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);

        startTime = System.nanoTime();
        for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = random.nextInt();
            treeMap.remove(key);
        }
        endTime = System.nanoTime();
        long durationTreeMap = TimeUnit.NANOSECONDS.toMillis(endTime - startTime);

        System.out.println("Delete Performance:");
        System.out.println("ART: " + durationArt + " ms");
        System.out.println("TreeMap: " + durationTreeMap + " ms");
    }
}

订单簿

  订单簿系统需要高效地处理和管理订单,以确保执行速度和数据准确性。在设计上需要考虑的点:

  1. 业务需求:

    • 价格优先:订单按价格优先级排序,高价买单和低价卖单优先匹配。
    • 下单:支持高效的订单插入操作,确保新订单能快速加入订单簿。
    • 撤单:支持高效的订单删除操作,确保用户可以快速撤销未成交订单。
    • 成交:支持高效的订单匹配和成交操作,确保订单能及时撮合成交。
  2. 数据结构需求:

    • 有序:订单簿需要维护订单的有序性,以便快速查找和匹配订单。
    • 高效查找/插入:支持高效的订单查找和插入操作,确保新订单能快速加入订单簿。
    • 高效查找/删除:支持高效的订单查找和删除操作,确保用户可以快速撤销未成交订单。
    • 查找最大/最小值,按序遍历:支持快速查找订单簿中的最大值和最小值,并按价格顺序遍历订单簿,确保订单匹配和撮合的效率。

  为了满足上述需求,订单簿系统应采用高效的数据结构,如自适应基数树(Adaptive Radix Tree, ART)、跳表(Skip List)或平衡二叉搜索树(如红黑树)。这些数据结构能够提供高效的查找、插入和删除操作,并支持按序遍历,确保订单簿系统的高性能和高可靠性。

ART树+订单簿的实验

  在观摩学习完论文后,在网上搜索了一个开源实现,然后开始进行订单簿的实验:

  github上开源实现:https://github.com/rohansuri/adaptive-radix-tree

  这个实现么有放到maven仓库当中,所以直接拿到工程目录来使用:

  代码放到这里了:https://github.com/aktiger/bitget-solution

   性能测试结果:https://github.com/aktiger/bitget-solution?tab=readme-ov-file#performance

  

 

后续改进

  需要结合现代处理器的特性进行特定优化。

  

 


总结: 

  时间比较有限,这个领域还是比较有意思的,以后有机会和时间可以进行更深入的研究。

 

参考:

1. https://www.oneyearago.me/2023/01/12/principle_and_optimization_of_radix_tree/

2. https://uncp.github.io/Adaptive-Radix-Tree/

3. https://b2broker.com/zh-hans/library/what-is-an-order-book-and-how-does-it-work/

 

posted @ 2025-03-29 23:43  justinzhang  阅读(155)  评论(0)    收藏  举报