C++ 跳过列表,它们真的表现得和Pugh paper声称的一样好吗?
我正在尝试使用最小的额外内存开销实现一个与BST性能一样好的跳过列表,目前即使不考虑任何内存限制,我的SkipList实现的性能也远远不是一个非常简单的平衡BST实现——也就是说,手工制作的BTS:)- 作为参考,我使用的是William Pugh的原始论文和我在Sedgewick-13.5-的C算法中找到的实现。我的代码是一个递归实现,以下是插入和查找操作的线索:C++ 跳过列表,它们真的表现得和Pugh paper声称的一样好吗?,c++,algorithm,performance,data-structures,skip-lists,C++,Algorithm,Performance,Data Structures,Skip Lists,我正在尝试使用最小的额外内存开销实现一个与BST性能一样好的跳过列表,目前即使不考虑任何内存限制,我的SkipList实现的性能也远远不是一个非常简单的平衡BST实现——也就是说,手工制作的BTS:)- 作为参考,我使用的是William Pugh的原始论文和我在Sedgewick-13.5-的C算法中找到的实现。我的代码是一个递归实现,以下是插入和查找操作的线索: sl_node* create_node() { short lvl {1}; while((dist2(en)&
sl_node* create_node()
{
short lvl {1};
while((dist2(en)<p)&&(lvl<max_level))
++lvl;
return new sl_node(lvl);
}
void insert_impl(sl_node* cur_node,
sl_node* new_node,
short lvl)
{
if(cur_node->next_node[lvl]==nullptr || cur_node->next_node[lvl]->value > new_node->value){
if(lvl<new_node->lvl){
new_node->next_node[lvl] = cur_node->next_node[lvl];
cur_node->next_node[lvl] = new_node;
}
if(lvl==0) return;
insert_impl(cur_node,new_node,lvl-1);
return;
}
insert_impl(cur_node->next_node[lvl],new_node,lvl);
}
sl_node* insert(long p_val)
{
sl_node* new_node = create_node();
new_node->value = p_val;
insert_impl(head, new_node,max_level-1);
return new_node;
}
sl_node* find_impl(sl_node* cur_node,
long p_val,
int lvl)
{
if(cur_node==nullptr) return nullptr;
if(cur_node->value==p_val) return cur_node;
if(cur_node->next_node[lvl] == nullptr || cur_node->next_node[lvl]->value>p_val){
if(lvl==0) return nullptr;
return find_impl(cur_node,p_val,lvl-1);
}
return find_impl(cur_node->next_node[lvl],p_val,lvl);
}
sl_node* find(long p_val)
{
return find_impl(head,p_val,max_level-1);
}
sl_节点-跳过列表节点-如下所示:
struct sl_node
{
long value;
short lvl;
sl_node** next_node;
sl_node(int l) : lvl(l)
{
next_node = new sl_node*[l];
for(short i{0};i<l;i++)
next_node[i]=nullptr;
}
~sl_node()
{
delete[] next_node;
}
};
struct Node
{
T value;
struct Node** next;
};
这几乎完美-哦,这是一个大的匹配文章中的理论,即:50%的1级,25%的2级,依此类推。输入数据来自我最好的伪随机数生成器,也称为std::random_设备,带有std::default_random_引擎和统一的int分布。输入在我看来非常随机:)
在我的机器上,以随机顺序搜索SkipList中262144个元素所需的时间为315ms,而在原始BTS上执行相同的搜索操作所需的时间为134ms,因此BTS的速度几乎是SkipList的两倍。这并不是我所期望的“跳过列表算法与平衡树具有相同的渐近预期时间界限,并且简单、快速、占用更少的空间”
“插入”节点所需的时间对于SkipList为387ms,对于BTS为143ms,同样,简单的BST性能更好
如果我不使用输入数字的随机序列,而是使用排序序列,那么事情就不会变得更有趣了,在这里,我糟糕的自制BST变得很慢,262144个排序整数的插入需要2866ms,而SkipList只需要168ms
但是,当谈到搜索时间时,BST仍然更快!对于排序输入,我们有234ms和77ms,此BST快3倍
对于不同的p因子值,我得到的性能结果略有不同:
最后但并非最不重要的一点是内存使用率图,正如您可能期望的那样,它确认了如果我们增加每个节点的级别数量,那么会显著影响内存指纹。内存使用量的计算方法与存储所有节点的附加指针所需的空间之和相同
在所有这些之后,你们中有谁能给我提供一个关于如何实现一个与BTS性能一样好的SkipList(不包括额外的内存开销)的评论吗
我知道DrDobbs上关于SkipList的文章,我看完了所有的文章,搜索和插入操作的代码与原始实现完全匹配,因此应该和我的一样好,而且这篇文章没有提供任何性能分析,我没有找到任何其他源代码。你能帮我吗
任何评论都将不胜感激
谢谢!:) 历史
自从威廉·普夫写了他的原始论文以来,时代已经发生了一些变化。我们在他的论文中没有提到CPU和操作系统的内存层次结构,这已成为当今的一个普遍焦点(现在通常与算法复杂性同等重要)
他的基准测试输入用例只有区区2^16个元素,而当时的硬件通常最多有32位扩展内存寻址可用。这使得指针的大小比我们现在在64位机器上使用的指针的大小小了一半或更小。同时,字符串字段(例如)可能同样大,使得存储在跳过列表中的元素与跳过节点所需的指针之间的比率可能要小得多,特别是考虑到每个跳过节点通常需要大量指针
在寄存器分配和指令选择等方面,C编译器在优化方面没有那么积极。即使是普通的手写程序集也常常可以在性能上提供显著的好处。像register
和inline
这样的编译器提示实际上在那段时间起了很大作用。虽然这看起来似乎没有什么实际意义,因为平衡的BST和跳过列表实现在这里是平等的,但即使是基本循环的优化也是一个更加手动的过程。当优化是一个越来越手动的过程时,更容易实现的东西往往更容易优化。跳过列表通常被认为比平衡树更容易实现
因此,所有这些因素可能都是普格当时结论的一部分。然而时代变了:硬件变了,操作系统变了,编译器变了,对这些主题做了更多的研究,等等
实施
除此之外,让我们玩一玩,实现一个基本的跳过列表。由于懒惰,我最终调整了可用的实现。这是一种普通的实现,与当今大量易于访问的示例性跳过列表实现几乎没有什么不同
我们将把实现的性能与std::set
进行比较,后者几乎总是以红黑树的形式实现*
*有些人可能想知道为什么我使用0
而不是nullptr
之类的东西。这是一种习惯。在我的工作场所,我们仍然需要编写面向各种编译器的开放库,包括那些只支持C++03的编译器,因此我仍然习惯于以这种方式编写低级/中级实现代码,有时甚至使用C,因此请原谅我编写此代码时使用的旧样式
。。。这太可怕了。在所有方面,我们的速度都是原来的两倍
优化
然而,我们可以做一个引人注目的优化。如果我们查看节点
,其当前字段如下所示:
struct sl_node
{
long value;
short lvl;
sl_node** next_node;
sl_node(int l) : lvl(l)
{
next_node = new sl_node*[l];
for(short i{0};i<l;i++)
next_node[i]=nullptr;
}
~sl_node()
{
delete[] next_node;
}
};
struct Node
{
T value;
struct Node** next;
};
这意味着节点字段的内存和它的下一个指针列表是两个独立的块,可能有很长的跨步
struct Node
{
T value;
struct Node** next;
};
[Node fields]-------------------->[next0,next1,...,null]
[Node fields,next0,next1,...,null]
struct Node
{
T value;
struct Node* next[1];
};
Node* create_node(int level, const T& new_value)
{
void* node_mem = malloc(sizeof(Node) + level * sizeof(Node*));
Node* new_node = static_cast<Node*>(node_mem);
new (&new_node->value) T(new_value);
for (int j=0; j < level+1; ++j)
new_node->next[j] = 0;
return new_node;
}
void destroy_node(Node* node)
{
node->value.~T();
free(node);
}
SkipSet (Before)
-- Inserted 200000 elements in 0.188765 secs
-- Found 200000 elements in 0.160895 secs
-- Erased 200000 elements in 0.162444 secs
SkipSet (After)
-- Inserted 200000 elements in 0.132322 secs
-- Found 200000 elements in 0.127989 secs
-- Erased 200000 elements in 0.130889 secs
Insertion
-- std::set: 0.104869 secs
-- SkipList: 0.132322 secs
Search:
-- std::set: 0.078351 secs
-- SkipList: 0.127989 secs
Removal:
-- std::set: 0.098208 secs
-- SkipList: 0.130889 secs
#include <iostream>
#include <iomanip>
#include <algorithm>
#include <cstdlib>
#include <ctime>
#include <cmath>
#include <vector>
#include <cassert>
#include <set>
using namespace std;
static const int max_level = 32;
class FixedAlloc
{
public:
FixedAlloc(): root_block(0), free_element(0), type_size(0), block_size(0), block_num(0)
{
}
FixedAlloc(int itype_size, int iblock_size): root_block(0), free_element(0), type_size(0), block_size(0), block_num(0)
{
init(itype_size, iblock_size);
}
~FixedAlloc()
{
purge();
}
void init(int new_type_size, int new_block_size)
{
purge();
block_size = max(new_block_size, type_size);
type_size = max(new_type_size, static_cast<int>(sizeof(FreeElement)));
block_num = block_size / type_size;
}
void purge()
{
while (root_block)
{
Block* block = root_block;
root_block = root_block->next;
free(block);
}
free_element = 0;
}
void* allocate()
{
assert(type_size > 0);
if (free_element)
{
void* mem = free_element;
free_element = free_element->next_element;
return mem;
}
// Create new block.
void* new_block_mem = malloc(sizeof(Block) - 1 + type_size * block_num);
Block* new_block = static_cast<Block*>(new_block_mem);
new_block->next = root_block;
root_block = new_block;
// Push all but one of the new block's elements to the free pool.
char* mem = new_block->mem;
for (int j=1; j < block_num; ++j)
{
FreeElement* element = reinterpret_cast<FreeElement*>(mem + j * type_size);
element->next_element = free_element;
free_element = element;
}
return mem;
}
void deallocate(void* mem)
{
FreeElement* element = static_cast<FreeElement*>(mem);
element->next_element = free_element;
free_element = element;
}
void swap(FixedAlloc& other)
{
std::swap(free_element, other.free_element);
std::swap(root_block, other.root_block);
std::swap(type_size, other.type_size);
std::swap(block_size, other.block_size);
std::swap(block_num, other.block_num);
}
private:
struct Block
{
Block* next;
char mem[1];
};
struct FreeElement
{
struct FreeElement* next_element;
};
// Disable copying.
FixedAlloc(const FixedAlloc&);
FixedAlloc& operator=(const FixedAlloc&);
struct Block* root_block;
struct FreeElement* free_element;
int type_size;
int block_size;
int block_num;
};
static double sys_time()
{
return static_cast<double>(clock()) / CLOCKS_PER_SEC;
}
static int random_level()
{
int lvl = 1;
while (rand()%2 == 0 && lvl < max_level)
++lvl;
return lvl;
}
template <class T>
class SkipSet
{
public:
SkipSet(): head(0)
{
for (int j=0; j < max_level; ++j)
allocs[j].init(sizeof(Node) + (j+1)*sizeof(Node*), 4096);
head = create_node(max_level, T());
level = 0;
}
~SkipSet()
{
while (head)
{
Node* to_destroy = head;
head = head->next[0];
destroy_node(to_destroy);
}
}
bool contains(const T& value) const
{
const Node* node = head;
for (int i=level; i >= 0; --i)
{
while (node->next[i] && node->next[i]->value < value)
node = node->next[i];
}
node = node->next[0];
return node && node->value == value;
}
void insert(const T& value)
{
Node* node = head;
Node* update[max_level + 1] = {0};
for (int i=level; i >= 0; --i)
{
while (node->next[i] && node->next[i]->value < value)
node = node->next[i];
update[i] = node;
}
node = node->next[0];
if (!node || node->value != value)
{
const int lvl = random_level();
assert(lvl >= 0);
if (lvl > level)
{
for (int i = level + 1; i <= lvl; ++i)
update[i] = head;
level = lvl;
}
node = create_node(lvl, value);
for (int i = 0; i <= lvl; ++i)
{
node->next[i] = update[i]->next[i];
update[i]->next[i] = node;
}
}
}
bool erase(const T& value)
{
Node* node = head;
Node* update[max_level + 1] = {0};
for (int i=level; i >= 0; --i)
{
while (node->next[i] && node->next[i]->value < value)
node = node->next[i];
update[i] = node;
}
node = node->next[0];
if (node->value == value)
{
for (int i=0; i <= level; ++i) {
if (update[i]->next[i] != node)
break;
update[i]->next[i] = node->next[i];
}
destroy_node(node);
while (level > 0 && !head->next[level])
--level;
return true;
}
return false;
}
void swap(SkipSet<T>& other)
{
for (int j=0; j < max_level; ++j)
allocs[j].swap(other.allocs[j]);
std::swap(head, other.head);
std::swap(level, other.level);
}
private:
struct Node
{
T value;
int num;
struct Node* next[1];
};
Node* create_node(int level, const T& new_value)
{
void* node_mem = allocs[level-1].allocate();
Node* new_node = static_cast<Node*>(node_mem);
new (&new_node->value) T(new_value);
new_node->num = level;
for (int j=0; j < level+1; ++j)
new_node->next[j] = 0;
return new_node;
}
void destroy_node(Node* node)
{
node->value.~T();
allocs[node->num-1].deallocate(node);
}
FixedAlloc allocs[max_level];
Node* head;
int level;
};
template <class T>
bool contains(const std::set<T>& cont, const T& val)
{
return cont.find(val) != cont.end();
}
template <class T>
bool contains(const SkipSet<T>& cont, const T& val)
{
return cont.contains(val);
}
template <class Set, class T>
void benchmark(int num, const T* elements, const T* search_elements)
{
const double start_insert = sys_time();
Set element_set;
for (int j=0; j < num; ++j)
element_set.insert(elements[j]);
cout << "-- Inserted " << num << " elements in " << (sys_time() - start_insert) << " secs" << endl;
const double start_search = sys_time();
int num_found = 0;
for (int j=0; j < num; ++j)
{
if (contains(element_set, search_elements[j]))
++num_found;
}
cout << "-- Found " << num_found << " elements in " << (sys_time() - start_search) << " secs" << endl;
const double start_erase = sys_time();
int num_erased = 0;
for (int j=0; j < num; ++j)
{
if (element_set.erase(search_elements[j]))
++num_erased;
}
cout << "-- Erased " << num_found << " elements in " << (sys_time() - start_erase) << " secs" << endl;
}
int main()
{
const int num_elements = 200000;
vector<int> elements(num_elements);
for (int j=0; j < num_elements; ++j)
elements[j] = j;
random_shuffle(elements.begin(), elements.end());
vector<int> search_elements = elements;
random_shuffle(search_elements.begin(), search_elements.end());
typedef std::set<int> Set1;
typedef SkipSet<int> Set2;
cout << fixed << setprecision(3);
for (int j=0; j < 2; ++j)
{
cout << "std::set" << endl;
benchmark<Set1>(num_elements, &elements[0], &search_elements[0]);
cout << endl;
cout << "SkipSet" << endl;
benchmark<Set2>(num_elements, &elements[0], &search_elements[0]);
cout << endl;
}
}
Insertion
-- std::set: 0.104869 secs
-- SkipList: 0.103632 secs
Search:
-- std::set: 0.078351 secs
-- SkipList: 0.089910 secs
Removal:
-- std::set: 0.098208 secs
-- SkipList: 0.089224 secs
std::set
-- Inserted 200000 elements in 0.044 secs
-- Found 200000 elements in 0.023 secs
-- Erased 200000 elements in 0.019 secs
SkipSet
-- Inserted 200000 elements in 0.027 secs
-- Found 200000 elements in 0.023 secs
-- Erased 200000 elements in 0.016 secs