《python解释器源码剖析》第7章--python中的set对象
7.0 序
集合和字典一样,都是性能非常高效的数据结构,性能高效的原因就在于底层使用了哈希表。因此集合和字典的原理本质上是一样的,都是把值映射成索引,通过索引去查找。
7.1 PySetObject
哈希表我们在字典那一章已经介绍过了,因此直接看set在cpython中的实现。
//python中的集合的每一个元素,是通过setentry这个结构体来存储的
typedef struct {
PyObject *key; // 元素的指针
Py_hash_t hash; // 元素的哈希值
} setentry;
typedef struct {
PyObject_HEAD
//我们发现在set中,每一个元素依然叫做entry
Py_ssize_t fill; /* active态以及dummy态的entry总数量*/
Py_ssize_t used; /* active态的entry数量 */
/* 该table包含mask+1个slot,mask+1是2的幂次方
我们存储的是mask,而不是size,因为更常需要mask
这个mask是用来和哈希值进行运算的
*/
Py_ssize_t mask;
/* 对于小表,该table指向固定大小的small table,对于bigger table则指向额外的malloc内存
该table的指针永远不会为NULL。
所以它是指向setentry数组的一个指针
*/
setentry *table;
Py_hash_t hash; /* 该PySetObject的哈希值,只适用于frozenset */
Py_ssize_t finger;
/*
用于pop元素的,search finger就是我们从包含某个元素的节点开始,找到我们希望的元素
*/
//smalltable就是显然就是一个保存了setentry类型的数组
//PySet_MINSIZE是一个宏定义,默认是8。如果元素比较少的话,存在smalltable里面
//当smalltable存不下的时候(仮),就会使用malloc申请。存不下,指的是超过8个的时候吗?
//由于哈希表的特性,需要预留一定的空间,因此还没存到8个的时候,就会扩容了
setentry smalltable[PySet_MINSIZE];
PyObject *weakreflist; /* 弱引用列表 */
} PySetObject;
7.2 PySetObject对象的创建
创建一个PySetObject对象可以使用PySet_New方法
PyObject *
PySet_New(PyObject *iterable)
{
//底层调用了make_new_set
return make_new_set(&PySet_Type, iterable);
}
static PyObject *
make_new_set(PyTypeObject *type, PyObject *iterable)
{
//申明一个PySetObject *指针
PySetObject *so;
//申请该元素所需要的内存
so = (PySetObject *)type->tp_alloc(type, 0);
//申请失败,返回NULL
if (so == NULL)
return NULL;
//初始化都为0
so->fill = 0;
so->used = 0;
//PySet_MINSIZE默认为8,mask初始化为7
so->mask = PySet_MINSIZE - 1;
//将table指向保存数据的smalltable的头指针
so->table = so->smalltable;
//初始化hash值为-1
so->hash = -1;
//finger为0
so->finger = 0;
//弱引用列表为NULL
so->weakreflist = NULL;
//如果迭代器不为NULL,那么把元素依次更新的so这个PySetObject中
if (iterable != NULL) {
if (set_update_internal(so, iterable)) {
Py_DECREF(so);
return NULL;
}
}
//返回初始化完成的set
return (PyObject *)so;
}
从以上步骤可以看出,初始化一个PySetObject对象主要初始化其内部的数据结构
7.3 插入元素
插入元素,会调用PySet_Add
int
PySet_Add(PyObject *anyset, PyObject *key)
{ //参数是两个指针
//类型检测
if (!PySet_Check(anyset) &&
(!PyFrozenSet_Check(anyset) || Py_REFCNT(anyset) != 1)) {
PyErr_BadInternalCall();
return -1;
}
//本质上调用了set_add_key
return set_add_key((PySetObject *)anyset, key);
}
static int
set_add_key(PySetObject *so, PyObject *key)
{
//声明一个变量,显然是存储哈希值的
Py_hash_t hash;
//类型检测
if (!PyUnicode_CheckExact(key) ||
(hash = ((PyASCIIObject *) key)->hash) == -1) {
//计算哈希值
hash = PyObject_Hash(key);
//如果传入的元素不能被hash,那么直接返回-1
//在python层面显然会报错
if (hash == -1)
return -1;
}
//底层又调用了set_add_entry,并把hash也作为参数传了进去
return set_add_entry(so, key, hash);
}
static int
set_add_entry(PySetObject *so, PyObject *key, Py_hash_t hash)
{
setentry *table; //指向setentry数组的指针,当然数组里面也是指针
setentry *freeslot;//存放不可hash的entry
setentry *entry;//entry指针
size_t perturb;
size_t mask;//和hash运算
size_t i; //一个整型变量,后面的索引值
size_t j;//遍历用的
int cmp;//比较的结果
/* Pre-increment is necessary to prevent arbitrary code in the rich
comparison from deallocating the key just before the insertion. */
Py_INCREF(key); //增加key的引用计数
restart:
mask = so->mask; //获取mask
i = (size_t)hash & mask;//mask和hash进行与运算,得到一个索引
entry = &so->table[i];//获取对应的entry指针
if (entry->key == NULL)
//如果entry->key == NULL,表示当前位置没有被使用
//直接跳到found_unused标签
goto found_unused;
//否则说明有人用了
freeslot = NULL;
perturb = hash; // 将perturb设置为hash
while (1) {
/*
找到entry->hash,之前说了,entry结构体由两部分组成
一个*key,也就是指向真正元素的指针,另一个是hash值
*/
//如果和我们当前的hash值一样的话
if (entry->hash == hash) {
//拿到当前的key
PyObject *startkey = entry->key;
/* startkey cannot be a dummy because the dummy hash field is -1 */
//entry里面的key不可以为dummy态,因为这相当于删除(伪删除)了,hash为-1
assert(startkey != dummy);
//如果已经存在的key和我们添加的key是一样,说明重复了
//而集合内的元素不允许重复
if (startkey == key)
//直接跳转到found_active标签
goto found_active;
//如果是unicode,那么先转化,然后再比较两个key是否一样
if (PyUnicode_CheckExact(startkey)
&& PyUnicode_CheckExact(key)
&& _PyUnicode_EQ(startkey, key))
//如果一样,跳转到found_active标签
goto found_active;
//那么获取头部指针
table = so->table;
//增加startkey的引用计数
Py_INCREF(startkey);
//不一样的话,通过富比较,去比较两个哈希值是否一致
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
//介绍startkey的引用计数
Py_DECREF(startkey);
//如果cmp大于0,比较成功
if (cmp > 0)
//说明索引被人占了
goto found_active;
if (cmp < 0)
//小于0说明比较失败
goto comparison_error;
/* 如果table或者entry改变了,我们必须从头开始 */
if (table != so->table || entry->key != startkey)
//跳转到restart标签
goto restart;
//拿到当前的mask
mask = so->mask; /* help avoid a register spill */
}
//如果不能hash
else if (entry->hash == -1)
//则设置为freeslot
freeslot = entry;
//如果当前索引值加上9小于当前的mask
//#define LINEAR_PROBES 9
if (i + LINEAR_PROBES <= mask) {
//循环9次
for (j = 0 ; j < LINEAR_PROBES ; j++) {
//每次得到下一个entry
entry++;
//如果hash=0,并且对应的key为NULL
if (entry->hash == 0 && entry->key == NULL)
//跳转到found_unused_or_dummy标签
goto found_unused_or_dummy;
if (entry->hash == hash) {
//如果hash值相同,获取对应的key
PyObject *startkey = entry->key;
//key必须不为dummy态
assert(startkey != dummy);
//如果两个key相同,跳转到found_active标签
if (startkey == key)
goto found_active;
//如果为unicode,还是转化后比较
if (PyUnicode_CheckExact(startkey)
&& PyUnicode_CheckExact(key)
&& _PyUnicode_EQ(startkey, key))
goto found_active;
//下面的跟if (i + LINEAR_PROBES <= mask) {上面的是一样的
table = so->table;
Py_INCREF(startkey);
cmp = PyObject_RichCompareBool(startkey, key, Py_EQ);
Py_DECREF(startkey);
if (cmp > 0)
goto found_active;
if (cmp < 0)
goto comparison_error;
if (table != so->table || entry->key != startkey)
goto restart;
mask = so->mask;
}
else if (entry->hash == -1)
freeslot = entry;
}
}
// 如果没有找到,说明哈希值冲突,改变规则,重新计算索引值
perturb >>= PERTURB_SHIFT;
//按照(i * 5 + 1 + perturb) & mask重新计算
i = (i * 5 + 1 + perturb) & mask;
//获取新索引对应的entry
entry = &so->table[i];
//如果对应的key为NULL,说明重新计算索引之后找到了可以存储的地方
if (entry->key == NULL)
//跳转到found_unused_or_dummy
goto found_unused_or_dummy;
//否则说明比较倒霉,改变规则重新映射索引依旧冲突
//那么继续循环,比较key是否一致等等
}
//未使用或者dummy,dummy我们是不可以使用的
found_unused_or_dummy:
//如果这个freeslot为NULL,说明是可用的
if (freeslot == NULL)
//跳转
goto found_unused;
//否则,说明为dummy态,那么我们依旧可以使用,正好废物利用
//将used数量加一
so->used++;
//设置key和hash值
freeslot->key = key;
freeslot->hash = hash;
return 0;
//发现未使用的
found_unused:
//将fill和used个数+1
so->fill++;
so->used++;
//设置key和hash值
entry->key = key;
entry->hash = hash;
//检查active态+dummy的entry个数是否小于mask的3/5
if ((size_t)so->fill*5 < mask*3)
//是的话,表示无需扩容
return 0;
//否则要进行扩容
//扩容的规则就是如果active态的entry各式各样如果大于50000,那么两倍扩容,否则四倍扩容
return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);
//如果是found_active,表示key重复了
//直接减少一个引用计数即可
found_active:
Py_DECREF(key);
return 0;
//比较失败,同样减少引用计数,返回-1
comparison_error:
Py_DECREF(key);
return -1;
}
总结一下流程就是:
传入hash值,计算出索引值,通过索引值找到对应的entry
如果entry->key=NULL,那么将hash和key存到对应的entry
如果有key,但是值相同,则不插入,直接减少引入计数。因为不是字典,不存在更新一说
如果有key,但是值不相同。那么从该索引往后的9个entry(i + 9 <= mask),如果存在key为NULL的entry,那么设置进去。
如果以上条件都不满足,那么改变策略重新计算索引值,直到找到一个满足key为NULL的entry
判断容量问题,如果active态+dummy态的entry个数不小于3/5 * mask,那么扩容,扩容的规则是active态的entry个数是否大于50000,是的话就二倍扩容,否则4倍扩容。
s = set()
item1 = 3 # hash(3) & 7 = 3
s.add(item1)
item2 = "satori" # hash("satori") & 7 = 2
s.add(item2)
7.4 PySetObject扩容
我们之前说PySetObject会改变容量,那么它是如何改变的呢?
static int
set_table_resize(PySetObject *so, Py_ssize_t minused)
{ //显然参数是:PySetObject *指针以及容量大小
//三个setentry *指针
setentry *oldtable, *newtable, *entry;
//oldmask
Py_ssize_t oldmask = so->mask;
//newmask
size_t newmask;
//是否为其申请过内存
int is_oldtable_malloced;
//将PySet_MINSIZE个entry直接copy过来
//因为你既然要扩容的话,那么肯定是这里面存不下了
setentry small_copy[PySet_MINSIZE];
//minused必须大于等于0
assert(minused >= 0);
/* Find the smallest table size > minused. */
/* XXX speed-up with intrinsics */
//newsize扩大二倍,直到大于minused
//所以我们刚才说的大于50000,二倍扩容,否则四倍扩容
//实际上是最终的newsize是比二倍或者四倍扩容的结果要大的
size_t newsize = PySet_MINSIZE;
while (newsize <= (size_t)minused) {
//newsize最大顶多也就是PY_SSIZE_T_MAX + 1,但是基本不可能存储这么多元素
newsize <<= 1; // The largest possible value is PY_SSIZE_T_MAX + 1.
}
/* Get space for a new table. */
//为新的table申请空间
oldtable = so->table;
assert(oldtable != NULL);
is_oldtable_malloced = oldtable != so->smalltable;
//如果newsize和PySet_MINSIZE(这里的8)相等
if (newsize == PySet_MINSIZE) {
/* A large table is shrinking, or we can't get any smaller. */
//拿到smalltable,就是默认初始化8个entry数组的那哥们
newtable = so->smalltable;
//如果oldtable和newtable一样
if (newtable == oldtable) {
//并且没有dummy态的entry
if (so->fill == so->used) {
/* No dummies, so no point doing anything. */
//那么无需做任何事情
return 0;
}
/* We're not going to resize it, but rebuild the
table anyway to purge old dummy entries.
Subtle: This is *necessary* if fill==size,
as set_lookkey needs at least one virgin slot to
terminate failing searches. If fill < size, it's
merely desirable, as dummies slow searches. */
//否则的话,dummy的个数一定大于0
assert(so->fill > so->used);
//扔掉dummy态,只把oldtable中active态的拷贝过来
memcpy(small_copy, oldtable, sizeof(small_copy));
//将small_copy重新设置为oldtable
oldtable = small_copy;
}
}
else {
//否则的话,肯定大于8,申请newsize个setentry所需要的空间
newtable = PyMem_NEW(setentry, newsize);
//如果newtable为NULL,那么申请内存失败,返回-1
if (newtable == NULL) {
PyErr_NoMemory();
return -1;
}
}
/* Make the set empty, using the new table. */
//newtable肯定不等于oldtable
assert(newtable != oldtable);
//创建一个能融安newsize个entry的空set
memset(newtable, 0, sizeof(setentry) * newsize);
//将mask设置为newsize-1
//将table设置为newtable
so->mask = newsize - 1;
so->table = newtable;
/* Copy the data over; this is refcount-neutral for active entries;
dummy entries aren't copied over, of course */
//获取newmask
newmask = (size_t)so->mask;
//将原来旧table的setentry数组里面所有setentry的key和hash值全部设置到新的table里面
if (so->fill == so->used) {
for (entry = oldtable; entry <= oldtable + oldmask; entry++) {
if (entry->key != NULL) {
set_insert_clean(newtable, newmask, entry->key, entry->hash);
}
}
} else {
so->fill = so->used;
for (entry = oldtable; entry <= oldtable + oldmask; entry++) {
if (entry->key != NULL && entry->key != dummy) {
set_insert_clean(newtable, newmask, entry->key, entry->hash);
}
}
}
//如果已经为其申请了内存,那么要将其归还给系统堆
if (is_oldtable_malloced)
PyMem_DEL(oldtable);
return 0;
}
//设置元素是通过set_insert_clean设置的
static void
set_insert_clean(setentry *table, size_t mask, PyObject *key, Py_hash_t hash)
{
setentry *entry;
size_t perturb = hash;
size_t i = (size_t)hash & mask; //计算索引
size_t j;
while (1) {
entry = &table[i]; //获取当前entry
if (entry->key == NULL)
goto found_null; //如果为空则跳转found_null设置key与hash
if (i + LINEAR_PROBES <= mask) {
//如果没有还是老规矩,遍历之后的9个entry
for (j = 0; j < LINEAR_PROBES; j++) {
entry++;
//找到空的entry,那么跳转到found_null设置key与hash
if (entry->key == NULL)
goto found_null;
}
}
// 没有找到,那么改变规则,重新计算索引
perturb >>= PERTURB_SHIFT;
i = (i * 5 + 1 + perturb) & mask;
}
found_null:
//设置key与hash
entry->key = key;
entry->hash = hash;
}
7.5 删除元素
static PyObject *
set_remove(PySetObject *so, PyObject *key)
{
PyObject *tmpkey;
int rv;
//将该值设置为dummy态
rv = set_discard_key(so, key);
if (rv < 0) {
//类型检测
if (!PySet_Check(key) || !PyErr_ExceptionMatches(PyExc_TypeError))
return NULL;
PyErr_Clear();
//对该值重新初始化为frozenset
tmpkey = make_new_set(&PyFrozenSet_Type, key);
if (tmpkey == NULL)
return NULL;
//将该key设置为空
rv = set_discard_key(so, tmpkey);
Py_DECREF(tmpkey);
if (rv < 0)
return NULL;
}
//如果没有找到则报错
if (rv == DISCARD_NOTFOUND) {
_PyErr_SetKeyError(key);
return NULL;
}
Py_RETURN_NONE;
}
//里面调用了set_discard_key方法
static int
set_discard_key(PySetObject *so, PyObject *key)
{
Py_hash_t hash;
//老套路,先计算hash值
if (!PyUnicode_CheckExact(key) ||
(hash = ((PyASCIIObject *) key)->hash) == -1) {
hash = PyObject_Hash(key);
if (hash == -1)
return -1;
}
//将hash值也船进入
return set_discard_entry(so, key, hash);
}
static int
set_discard_entry(PySetObject *so, PyObject *key, Py_hash_t hash)
{
setentry *entry;
PyObject *old_key;
////通过传入的key和hash找到该entry
//并且hash对应的key要和传入的key是一样的
entry = set_lookkey(so, key, hash);
//如果entry为NULL,直接返回-1
if (entry == NULL)
return -1;
//如果entry不为NULL,但是对应的key为NULL
//返回DISCARD_NOTFOUND
if (entry->key == NULL)
return DISCARD_NOTFOUND;
//获取要删除的key
old_key = entry->key;
//并将entry的key设置为dummy
entry->key = dummy;
//hash值设置为-1
entry->hash = -1;
//减少使用数量
so->used--;
//减少引用计数
Py_DECREF(old_key);
//返回DISCARD_FOUND
return DISCARD_FOUND;
}
可以看到集合添加、删除元素和字典是有些相似的,毕竟底层都是使用了hash表嘛
7.6 集合的运算(交集)
在python中使用集合的时候,可以取两个集合的交集、并集、差集、对称差集等等,这里介绍一下交集,其余的可以自行看源码研究(Objects/setobject.c
)。
static PyObject *
set_intersection(PySetObject *so, PyObject *other)
{ //显然是两个指针,一个是PySetObject *,一个是PyObject *
//result,显然是用来存储两者交集运算的结果的
PySetObject *result;
//不看下面代码的话,很难知道这几个PyObject * 是用来干啥的
//我们下面代码再看看这是干啥的
PyObject *key, *it, *tmp;
//这个肯定是hash值
Py_hash_t hash;
int rv;
//如果两个对象一样
if ((PyObject *)so == other)
//直接返回其中一个的拷贝即可
return set_copy(so);
//这行代码表示创建一个空的PySetObject *
result = (PySetObject *)make_new_set_basetype(Py_TYPE(so), NULL);
//如果result == NULL,说明创建失败
if (result == NULL)
return NULL;
//检测other是不是PySetObject *
if (PyAnySet_Check(other)) {
//初始索引为0
Py_ssize_t pos = 0;
//setentry *
setentry *entry;
//如果other元素的个数大于so
if (PySet_GET_SIZE(other) > PySet_GET_SIZE(so)) {
//就把so和other进行交换
tmp = (PyObject *)so;
so = (PySetObject *)other;
other = tmp;
}
//从少的那一方的开头开始便利
while (set_next((PySetObject *)other, &pos, &entry)) {
//拿到key和hash
key = entry->key;
hash = entry->hash;
//传入other的key和hash,在so中去找
rv = set_contains_entry(so, key, hash);
if (rv < 0) {
//如果对应的rv不存在,那么显然就没有
Py_DECREF(result);
return NULL;
}
if (rv) {
//存在的话设置进result里面
if (set_add_entry(result, key, hash)) {
Py_DECREF(result);
return NULL;
}
}
}
//直接返回
return (PyObject *)result;
}
//如果不是PyObject *
//那么获取其对应的迭代器,相当于python中的__iter__
it = PyObject_GetIter(other);
//如果是NULL,降低其引用计数
if (it == NULL) {
Py_DECREF(result);
//返回NULL
return NULL;
}
//下面的没必要分析了,在python中,只能set和set(或者frozenset)之间才可以取交集
while ((key = PyIter_Next(it)) != NULL) {
hash = PyObject_Hash(key);
if (hash == -1)
goto error;
rv = set_contains_entry(so, key, hash);
if (rv < 0)
goto error;
if (rv) {
if (set_add_entry(result, key, hash))
goto error;
}
Py_DECREF(key);
}
Py_DECREF(it);
if (PyErr_Occurred()) {
Py_DECREF(result);
return NULL;
}
return (PyObject *)result;
error:
Py_DECREF(it);
Py_DECREF(result);
Py_DECREF(key);
return NULL;
}
7.7 まとめ
可以看到,剖析set的时候话很少。主要是有了剖析dict的经验,因此再剖析set的时候就很简单了。并且在python中还有一个frozenset,就是不可变的set。而且不像list和tuple,tuple还是有很多特殊的,并不单单只是不可变的list,从具有自己独自的结构体就能看出来。而frozenset和set都是一个结构体,只有一个PySetObject,没有PyFrozenSetObject,我们在看PySetObject的时候,发现里面有一个hash值,如果是frozenset的话,那么hash值是不为-1的,因为它不可以添加、删除元素,是不可变对象。因此frozenset就不单独开一个章节介绍了,可以的话,自己看一下源码。源码还是Object/setobject.c
。
《python解释器源码剖析》第7章--python中的set对象的更多相关文章
- 《python解释器源码剖析》第13章--python虚拟机中的类机制
13.0 序 这一章我们就来看看python中类是怎么实现的,我们知道C不是一个面向对象语言,而python却是一个面向对象的语言,那么在python的底层,是如何使用C来支持python实现面向对象 ...
- 《python解释器源码剖析》第12章--python虚拟机中的函数机制
12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python ...
- 《python解释器源码剖析》第9章--python虚拟机框架
9.0 序 下面我们就来剖析python运行字节码的原理,我们知道python虚拟机是python的核心,在源代码被编译成字节码序列之后,就将有python的虚拟机接手整个工作.python虚拟机会从 ...
- 《python解释器源码剖析》第0章--python的架构与编译python
本系列是以陈儒先生的<python源码剖析>为学习素材,所记录的学习内容.不同的是陈儒先生的<python源码剖析>所剖析的是python2.5,本系列对应的是python3. ...
- 《python解释器源码剖析》第1章--python对象初探
1.0 序 对象是python中最核心的一个概念,在python的世界中,一切都是对象,整数.字符串.甚至类型.整数类型.字符串类型,都是对象.换句话说,python中面向对象的理念观测的非常彻底,面 ...
- 《python解释器源码剖析》第11章--python虚拟机中的控制流
11.0 序 在上一章中,我们剖析了python虚拟机中的一般表达式的实现.在剖析一遍表达式是我们的流程都是从上往下顺序执行的,在执行的过程中没有任何变化.但是显然这是不够的,因为怎么能没有流程控制呢 ...
- 《python解释器源码剖析》第8章--python的字节码与pyc文件
8.0 序 我们日常会写各种各样的python脚本,在运行的时候只需要输入python xxx.py程序就执行了.那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢? 8. ...
- 《python解释器源码剖析》第4章--python中的list对象
4.0 序 python中的list对象,底层对应的则是PyListObject.如果你熟悉C++,那么会很容易和C++中的list联系起来.但实际上,这个C++中的list大相径庭,反而和STL中的 ...
- 《python解释器源码剖析》第2章--python中的int对象
2.0 序 在所有的python内建对象中,整数对象是最简单的对象.从对python对象机制的剖析来看,整数对象是一个非常好的切入点.那么下面就开始剖析整数对象的实现机制 2.1 初识PyLongOb ...
随机推荐
- openstack核心组件--neutron网络服务(4)
一.neutron 介绍: Neutron 概述 传统的网络管理方式很大程度上依赖于管理员手工配置和维护各种网络硬件设备:而云环境下的网络已经变得非常复杂,特别是在多租户场景里,用户随时都可能需要 ...
- 配置yum镜像源
centos7配置本地yum源 先从官网下载centos7镜像 以centos7.4 为例 CentOS-7-x86_64-Everything-1804 [root@kangvcar ~]# mv ...
- androidstudio的安装与抓log
公司新开发的软件是基于unity的,我们被告知unity是不允许charles抓包的,故此只能只用 Android studio 一. Android studio的安装与配置 在此我就不赘 ...
- Django:(05)类视图,装饰器和中间件
一.类视图的定义和使用 在Django中还可以通过类来定义一个视图,称为类视图. 定义一个类视图:定义一个类,需继承 Django 提供的 View 类 . from django.views.gen ...
- Unreal Engine* 4/英特尔® VTune™ Amplifier 使用指南
借助英特尔 VTune Amplifier,可以通过单一易用的分析界面获得先进的分析功能.UE4 和英特尔 VTune Amplifier 相互配合,支持调查代码并进行分析,从而在多个内核上顺畅运行. ...
- 安装neutron
在控制节点上执行 controllerHost='controller' MYSQL_PASSWD='m4r!adbOP' RABBIT_PASSWD='0penstackRMQ' NOVA_PASS ...
- B. Grow The Tree Codeforces Round #594 (Div. 2)
Gardener Alexey teaches competitive programming to high school students. To congratulate Alexey on t ...
- vim 常用基本
vim 基本操作 0. 基本操作 :w // 保存当前文件 :q // 退出vim :wq // 保存退出 :w! // 强制保存当前文件 :q! // 强制退出(可以忽略修改) :!cmd // 执 ...
- 黑科技——树剖两次dfs转一次dfs!
黑科技--树剖两次\(dfs\)转一次\(dfs\)! 重所周知,树链剖分通常是要\(dfs\)两次的,就像这样: int Fa[N],dep[N],Sz[N],son[N]; void dfs1( ...
- linux常用终端命令(一)终端命令格式(二)文件和目录常用命令
一.linux终端命令格式 1.终端命令格式 command [-options] [parameter] 说明: command :命令名,相应功能的英文单词或单词的缩写 [-options] ...