首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >缓存感知,基于4位(16时隙) Trie (前缀树)的密钥/值字典,具有延迟分配。

缓存感知,基于4位(16时隙) Trie (前缀树)的密钥/值字典,具有延迟分配。
EN

Code Review用户
提问于 2015-07-16 21:23:16
回答 1查看 142关注 0票数 5

延迟分配是在“桶”中只有一个项时使用Node**作为Node*的内存优化技巧。因此," item“成员要么保存此类孤独项的索引,要么保存一个负值(如果桶被完全分配)。

这里的缓存感知仅仅是在正确的位置添加编译器的__builtin_prefetch(),这样就可以提高大约30%的速度。

trie4d.h

代码语言:javascript
运行
复制
#ifndef TRIE4D_H

#include <stdlib.h>
#include <sys/types.h>
#include <string.h>

/* define your own value type if needed */
#define VALUE_TYPE int

struct Node {
    struct Node **N;
    VALUE_TYPE value;
    char item;
};

struct Node* newNode();
int addNode(struct Node *C, void *key, int count, VALUE_TYPE value);
struct Node* findNode(struct Node * C, void *key, int count);
void freeNode(struct Node* C);

#endif // TRIE4D_H

trie4d.c

代码语言:javascript
运行
复制
#include "trie4d.h"

struct Node* newNode() {
    struct Node *node;
    node = malloc(sizeof (struct Node));
    node->value = -1;
    node->item = -1;
    return node;
}

int addNode(struct Node *C, void *key, int count, VALUE_TYPE value) {
    int pos = 0;
    struct Node* tmp;
    while (1) {
        char u;
        int bpos = pos >> 1;
        if (bpos >= count) {
            if (C->value == -1) {
                C->value = value;
                return 1; /* value added */
            }
            C->value = value;
            return 0; /* value replaced */
        }
        unsigned char b = ((unsigned char*)key)[bpos];
        if (pos++ & 1) u = (b >>= 4);
        else u = b & 0xf;
        if (C->item == -1) {
            C->item = u;
            C->N = (struct Node**)newNode();
            C = (struct Node*) C->N;
        }
        else if (C->item >= 0) {
            if (C->item == u) {
                C = (struct Node*)C->N;
            }
            else {
                tmp = (struct Node*) C->N;
                C->N = malloc(sizeof (void*) * 16);
                memset(C->N, 0, sizeof(void*) << 4);
                C->N[C->item] = tmp;
                C->item = -2;
                C = C->N[u] = newNode();
            }
        } else {
            if (C->N[u])
                C = C->N[u];
            else 
                C = C->N[u] = newNode();
        }
    }
}

struct Node* findNode(struct Node * C, void *key, int count) {
    int pos = 0;
    while (1) {
        char u;
        int bpos = pos >> 1;
        if (bpos >= count) return C;
        unsigned char b = ((unsigned char*)key)[bpos];
        if (pos++ & 1) u = (b >>= 4);
        else u = b & 0xf;

        if (C->item == -1) return 0;
        else if (C->item >= 0) {
            if (u == C->item) C = (struct Node*)C->N;
            else return 0;
        } else {
            __builtin_prefetch(C->N[u]);
            C = C->N[u];
            if (C == 0) return 0;
        }
    }
}

#ifdef DEBUG
/* this will count upon destruction, how much memory was used */
int mem_count = 0;
#endif

void freeNode(struct Node* C) {
    int i;
    if (C->item == -2) {
        for (i = 0; i < 16; i++) {
            if (C->N[i]) freeNode(C->N[i]);
        }
        free(C->N);
        #ifdef DEBUG
        mem_count += 16 * sizeof(struct Node);
        #endif
    }
    else if (C->item >= 0) {
        freeNode((struct Node*)C->N);
    }
    free(C);
    #ifdef DEBUG
    mem_count += sizeof(struct Node);
    #endif
}

这个trie主要是作为非常快的键/值字典(与哈希表竞争)制作的,因此您将找不到用于尝试函数“从节点遍历”(对于“自动完成”样式实现有用)的通用函数。

虽然这个实现比哈希表使用更多的内存,但是它几乎同样快而且非常简单,只有100个LOC。使之成为移植到低级语言(如程序集或Forth)的好人选。

我们的目标是制作一个具有O(K)查找时间的非常快的字典,其中K是一个键的长度。问题是,现代CPU在指针删除时非常慢,而树的本质是在每一步上执行指针取消引用,因此应该重新加载缓存。

EN

回答 1

Code Review用户

发布于 2015-07-22 02:35:03

通用评论

总的来说,我发现您的代码相当合理。您使用可变长度键的每个4位作为"trie“的”字符“。

  1. 我认为您应该定义这些常量,而不是使用神奇的数字:# ITEM_EMPTY -1 #定义ITEM_ALLOCATED -2 #定义VALUE_UNSET -1
  2. 当您分配一个新节点时,您将node->item设置为-1,但不将node->N初始化为任何东西。这是因为您的代码中有一个隐含的假设,即如果是node->item == -1,那么node->N是不可用的。但是在调试时,为了您自己的理智,我建议您无论如何设置node->N = NULL
  3. 在这两行代码中: C->N = malloc(sizeof of (void*) * 16);memset(C->N,0,sizeof of (void*) << 4);首先,您不一致,因为您在下一个位置使用了* 16 one place和<< 4。但实际上,您应该将该代码替换为: C->N = calloc(16,sizeof(void *));
  4. 如果/else样式和大括号不一致。比较: if (C->item == u) {C= (struct Node*)C->N;} else {具有: if (C->item == -1)返回0;如果(C->项目>= 0) { if (u == C->item) C= (struct Node*)C->N;否则返回0;}=={
  5. 你的可变名字有点神秘。我最终发现bpos是“字节位置”,而pos是“咬点位置”。不过,我还是不知道Cu代表什么。我认为count应该改名为keyLength

加载每个字节两次

在您的addNode()findNode()函数中,您从键加载一个字节,并使用其上半部分或下半部分(每trie深度4位)。因此,您最终会从键中加载两次字节(每一半加载一次)。您可以通过保存该字节的值来做得更好,并且在第二次迭代中,您可以使用保存的值来获取上半部分,而不是从内存中重新加载该字节。

预取

如前所述,我不认为您的__builtin_prefetch()调用会有任何作用。在要取消对指针的引用之前,就把预取调用放在这里,所以即使编译器在那里插入了预取指令,也无助于加快速度。我编译了你的程序,不管有没有这一行,这两个版本都生成了相同的汇编代码(没有预取指令)。从builtin_prefetch()中获得好处的方法是,在您真正使用该地址之前,将其称为一段时间。例如,在while循环的顶部,您可以这样做:

代码语言:javascript
运行
复制
if (C->item != -1)
    __builtin_prefetch(C->N);

不过,我不确定你会从中得到多少好处。您将不得不运行一些测试,以确定这是否真的有帮助或伤害。

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/97169

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档