Quantcast
Channel: 英特尔开发人员专区文章
Viewing all articles
Browse latest Browse all 583

使用持久内存提升 C++ 应用的性能-一个简单的 grep 示例

$
0
0

下载示例代码

简介

本文展示了如何转换简单的 C++ 程序,选取了著名的 UNIX 命令行实用程序的简化版本 grep 作为示例,以利用持久内存(PMEM)。本文提供了详细的代码,首先描述了易失性版 grep 程序的作用。然后讨论了如何将 grep 添加至搜索结果的持久高速缓存中,以改进 grep。高速缓存通过1添加容错(FT)功能和2加速对已发现搜索模式的查询改进了 grep。本文使用 libpmemobj 的 C++ 绑定继续描述 grep 的持久版本,libpmemobj 是 持久内存开发套件(PMDK)集中的一个核心库。最后,使用线程和 PMEM 感知型同步添加了并行处理(在文件粒度中)。

易失性 Grep

描述

如果您熟悉任何类似于 UNIX* 的操作系统,如 GNU/Linux*,可能也比较了解命令行实用程序 grep(代表以正规表示法进行全域查找并打印)。实质上,grep 接收两种参数(其余是选项):采用正则表达式形式的模式和输入文件(包括标准输入)。grep 的目标是逐行扫描输入,然后输入匹配给定模式的行。更多详情敬请参阅 grep 手册页面(在终端输入手册 grep 或查看 Linux 手册页面,以在线了解 grep)。

我的简化版 grep 只使用了上述两种参数(模式输入),输入应为单个文件或目录。如果提供了目录,目录内容将被扫描,以查找输入文件(通常以递归的形式扫描子目录)。为了查看实际的运行方式,我们将它的源代码用作输入,将“int”用作模式,并运行该程序。

代码可从 GitHub* 中下载。为了从 pmdk-examples 存储库的根编译代码,输入 make simple-grep。 libpmemobj和 C++ 编译器必须安装在您的系统。为了兼容 Windows* 操作系统,代码不会调用任何针对 Linux 的函数。相反,使用 Boost C++ 库集合(主要用于处理文件系统输入/输出)。如果您使用 Linux,可能已面向您最喜爱的分发版配备了 Boost C++。例如,在 Ubuntu* 16.04 中,您可以通过以下方式安装这些库:

# sudo apt-get install libboost-all-dev

如果程序得以正确编译,我们可以按照以下方式运行程序:

$ ./grep int grep.cpp
FILE = grep.cpp
44: int
54:     int ret = 0;
77: int
100: int
115: int
135: int
136: main (int argc, char *argv[])

如您所见,grep 共找到 7 行包含“int”的代码(第 44、54、77、100、115、135 和 136 行)。作为一项合理性检查,我们可以使用系统提供的 grep 运行相同的查询:

$ grep int –n grep.cpp
44:int
54:     int ret = 0;
77:int
100:int
115:int
135:int
136:main (int argc, char *argv[])

截至目前,我们已经得到了预期的输出。代码如下表所示(注:以上代码片段中的行数与下表不匹配,因为代码格式不同于初始源文件):

#include <boost/filesystem.hpp>
#include <boost/foreach.hpp>
#include <fstream>
#include <iostream>
#include <regex>
#include <string.h>
#include <string>
#include <vector>

using namespace std;
using namespace boost::filesystem;
/* auxiliary functions */
int
process_reg_file (const char *pattern, const char *filename)
{
        ifstream fd (filename);
        string line;
        string patternstr ("(.*)(");
        patternstr += string (pattern) + string (")(.*)");
        regex exp (patternstr);

        int ret = 0;
        if (fd.is_open ()) {
                size_t linenum = 0;
                bool first_line = true;
                while (getline (fd, line)) {
                        ++linenum;
                        if (regex_match (line, exp)) {
                                if (first_line) {
                                        cout << "FILE = "<< string (filename);
                                        cout << endl << flush;
                                        first_line = false;
                                }
                                cout << linenum << ": "<< line << endl;
                                cout << flush;
                        }
                }
        } else {
                cout << "unable to open file " + string (filename) << endl;
                ret = -1;
        }
        return ret;
}

int
process_directory_recursive (const char *dirname, vector<string> &files)
{
        path dir_path (dirname);
        directory_iterator it (dir_path), eod;

        BOOST_FOREACH (path const &pa, make_pair (it, eod)) {
                /* full path name */
                string fpname = pa.string ();
                if (is_regular_file (pa)) {
                        files.push_back (fpname);
                } else if (is_directory (pa) && pa.filename () != "."&& pa.filename () != ".."){
                        if (process_directory_recursive (fpname.c_str (), files)
                            < 0)
                                return -1;
                }
        }
        return 0;
}

int
process_directory (const char *pattern, const char *dirname)
{
        vector<string> files;
        if (process_directory_recursive (dirname, files) < 0)
                return -1;
        for (vector<string>::iterator it = files.begin (); it != files.end ();
             ++it) {
                if (process_reg_file (pattern, it->c_str ()) < 0)
                        cout << "problems processing file "<< *it << endl;
        }
        return 0;
}

int
process_input (const char *pattern, const char *input)
{
        /* check input type */
        path pa (input);
        if (is_regular_file (pa))
                return process_reg_file (pattern, input);
        else if (is_directory (pa))
                return process_directory (pattern, input);
        else {
                cout << string (input);
                cout << " is not a valid input"<< endl;
        }
        return -1;
}

/* MAIN */
int
main (int argc, char *argv[])
{
        /* reading params */
        if (argc < 3) {
                cout << "USE "<< string (argv[0]) << " pattern input ";
                cout << endl << flush;
                return 1;
        }
        return process_input (argv[1], argv[2]);
}

我知道代码很长,但是相信我,代码不难运行。我只需要在 process_input()中检查输入是一个文件,还是一个目录。如果是前一种情况,将会在 process_reg_file()中直接处理文件。如果是后一种情况,将会在 process_directory_recursive()中扫描目录下的文件,然后通过调用每个文件上的 process_reg_file(),在 process_directory()中逐一处理被扫描的文件。处理文件时,检查每行是否匹配模式。如果匹配,将该行打印为标准输出。

持久内存

现在我们得到了一个正常运行的 grep,我们看一下如何对它进行改进。首先,我们发现 grep 不会保存任何状态。完成了对输入的分析并生成输出后,程序随即停止。假设我们计划每周都对一个大型目录(拥有几十万份文件)进行扫描,以查找相关的特定模式。假设目录中的文件可能会不断变化(尽管所有文件不可能同时改变),也可能会添加新的文件。如果我们使用经典的 grep 执行该任务,可能会重复扫描某些文件,浪费了宝贵的 CPU 周期。这个限制可以通过添加一个高速缓存来克服:如果已经针对特定模式对文件进行了扫描(而且上次扫描之后内容没有发生变化),grep 将返回缓存的结果,而不是重新扫描文件。

可以通过多种方式实施高速缓存。例如,一种方法是创建一个特定的数据库(DB),以存储每个被扫描文件和模式的结果(还会添加一个时间戳,以检测文件修改)。虽然该方法行之有效,但是,不要求安装与运行 DB 引擎的解决方案将是一个更好的选择,况且该方法需要在每次分析文件时,执行 DB 查询(将产生网络与输入/输出开销)。另一种方法是将该缓存存储为常规文件。在开始时,将缓存加载至易失性内存,在执行结束时或每次分析新文件时,对其进行更新。这种方法看似好用,但是我们不得不创建两个数据模型,一个用于易失性 RAM,另一个用于二级持久存储(文件),并且需要写入代码,以便在两个模型之间反复转化。如果能够避免这种额外的编码工作,那就最好不过。

持久 Grep

设计注意事项

使用 libpmemobj 编写 PEME 感知型代码的第一步通常是设计需要持久存储的数据对象类型。根对象是需要定义的第一类对象。该对象具有强制性,用于固定 PMEM 池中创建的所有其他对象(将池视为 PMEM 设备中的文件)。我的 grep 示例使用了以下持久数据结构:


图 1.PMEM 感知型 grep 的数据结构。

通过创建从根类中挂起的模式链表来整理高速缓存数据。每当搜索新模式时,将会创建一个新的类模式对象。如果当前搜索的模式在过去被搜索过,便无需创建对象(模式字符串被存储于 patternstr)。我们从类模式中挂起被扫描文件的链表。文件包括名称(在本示例中,名称和文件系统路径相同)、修改时间(用于检查文件是否已被修改)和匹配该模式的行的矢量。我们只针对未被扫描的文件创建新的类文件对象。

首先需要注意的是特殊类p<>(面向基础类型)和 persistent_ptr<>(面向复杂类型的指针)。这些类被用于通知库在交易时注意这些内存区(发生故障时,记录与回滚对象的更改)。得益于虚拟内存的性质,persistent_ptr<>将始终适用于 PMEM 中的指针。当池被程序打开,并且映射至虚拟内存地址空间时,池的位置可能与同一程序(或者访问同一个池的其他程序)使用的先前位置不同。在 PMDK 的示例中,持久指针被实施为胖指针;它们包含一个池 ID(用于从转换表中访问当前的池虚拟地址)+偏置(在池开始时)。有关 PMDK 中指针的更多信息,请参阅libpmemobj 中类型安全的宏面向 libpmemobj 的 C++ 绑定(第 2 部分)-持久智能指针

您可能想知道为什么行的矢量(std::vector)不被声明为持久指针。原因是没有必要这样做。表示矢量和行的对象一经创建(类文件对象的构建过程中),便不会改变。因此,无需在交易过程中追踪对象。尽管如此,矢量本身也会在内部分配(与删除)对象。因此,我们不能单纯依靠来自 std::vector的默认分配器(该分配器只了解易失性内存,分配堆中的所有对象);我们需要传输 libpmemobj 提供的定制化分配器(了解 PMEM)。该分配器为 pmem::obj::allocator<line>。我们以这种方式声明矢量后,便可以按照任意正常易失性代码中的使用方式使用它。事实上,您可以以这种方式使用任何标准容器类。

代码修改

现在,我们跳至代码部分。为了避免重复,只列出新代码(完整代码可在 pmemgrep/pmemgrep.cpp 中获取)。我们从定义(新的标头、宏、命名空间、全局变量和类)着手:

...
#include <libpmemobj++/allocator.hpp>
#include <libpmemobj++/make_persistent.hpp>
#include <libpmemobj++/make_persistent_array.hpp>
#include <libpmemobj++/persistent_ptr.hpp>
#include <libpmemobj++/transaction.hpp>
...
#define POOLSIZE ((size_t) (1024 * 1024 * 256)) /* 256 MB */
...
using namespace pmem;
using namespace pmem::obj;

/* globals */
class root;
pool<root> pop;

/* persistent data structures */
struct line {
	persistent_ptr<char[]> linestr;
	p<size_t> linenum;
};

class file
{
	private:

	persistent_ptr<file> next;
	persistent_ptr<char[]> name;
	p<time_t> mtime;
	vector<line, pmem::obj::allocator<line>> lines;

	public:

	file (const char *filename)
	{
		name = make_persistent<char[]> (strlen (filename) + 1);
		strcpy (name.get (), filename);
		mtime = 0;
	}

	char * get_name (void) { return name.get (); }

	size_t get_nlines (void) { return lines.size (); /* nlines; */ }

	struct line * get_line (size_t index) { return &(lines[index]); }

	persistent_ptr<file> get_next (void) { return next; }

	void set_next (persistent_ptr<file> n) { next = n; }

	time_t	get_mtime (void) { return mtime; }

	void set_mtime (time_t mt) { mtime = mt; }

	void
	create_new_line (string linestr, size_t linenum)
	{
		transaction::exec_tx (pop, [&] {
			struct line new_line;
			/* creating new line */
			new_line.linestr
			= make_persistent<char[]> (linestr.length () + 1);
			strcpy (new_line.linestr.get (), linestr.c_str ());
			new_line.linenum = linenum;
			lines.insert (lines.cbegin (), new_line);
		});
	}

	int
	process_pattern (const char *str)
	{
		ifstream fd (name.get ());
		string line;
		string patternstr ("(.*)(");
		patternstr += string (str) + string (")(.*)");
		regex exp (patternstr);
		int ret = 0;
		transaction::exec_tx (
		pop, [&] { /* dont leave a file processed half way through */
		      if (fd.is_open ()) {
			      size_t linenum = 0;
			      while (getline (fd, line)) {
				      ++linenum;
				      if (regex_match (line, exp))
					      /* adding this line...*/
					      create_new_line (line, linenum);
			      }
		      } else {
			      cout
			      << "unable to open file " + string (name.get ())
			      << endl;
			      ret = -1;
		      }
		});
		return ret;
	}

	void remove_lines () { lines.clear (); }
};

class pattern
{
	private:

	persistent_ptr<pattern> next;
	persistent_ptr<char[]> patternstr;
	persistent_ptr<file> files;
	p<size_t> nfiles;

	public:

	pattern (const char *str)
	{
		patternstr = make_persistent<char[]> (strlen (str) + 1);
		strcpy (patternstr.get (), str);
		files = nullptr;
		nfiles = 0;
	}

	file *
	get_file (size_t index)
	{
		persistent_ptr<file> ptr = files;
		size_t i = 0;
		while (i < index && ptr != nullptr) {
			ptr = ptr->get_next ();
			i++;
		}
		return ptr.get ();
	}

	persistent_ptr<pattern> get_next (void)	{ return next; }

	void set_next (persistent_ptr<pattern> n) { next = n; }

	char * get_str (void) { return patternstr.get (); }

	file *
       find_file (const char *filename) {
		persistent_ptr<file> ptr = files;
		while (ptr != nullptr) {
			if (strcmp (filename, ptr->get_name ()) == 0)
				return ptr.get ();
			ptr = ptr->get_next ();
		}
		return nullptr;
	}

	file *
       create_new_file (const char *filename) {
		file *new_file;
		transaction::exec_tx (pop, [&] {
			/* allocating new files head */
			persistent_ptr<file> new_files
			= make_persistent<file> (filename);
			/* making the new allocation the actual head */
			new_files->set_next (files);
			files = new_files;
			nfiles = nfiles + 1;
			new_file = files.get ();
		});
		return new_file;
	}

	void
	print (void)
	{
		cout << "PATTERN = "<< patternstr.get () << endl;
		cout << "\tpattern present in "<< nfiles;
		cout << " files"<< endl;
		for (size_t i = 0; i < nfiles; i++) {
			file *f = get_file (i);
			cout << "###############"<< endl;
			cout << "FILE = "<< f->get_name () << endl;
			cout << "###############"<< endl;
			cout << "*** pattern present in "<< f->get_nlines ();
			cout << " lines ***"<< endl;
			for (size_t j = f->get_nlines (); j > 0; j--) {
				cout << f->get_line (j - 1)->linenum << ": ";
				cout
				<< string (f->get_line (j - 1)->linestr.get ());
				cout << endl;
			}
		}
	}
};

class root
{
	private:

	p<size_t> npatterns;
	persistent_ptr<pattern> patterns;

	public:

	pattern *
	get_pattern (size_t index)
	{
		persistent_ptr<pattern> ptr = patterns;
		size_t i = 0;
		while (i < index && ptr != nullptr) {
			ptr = ptr->get_next ();
			i++;
		}
		return ptr.get ();
	}

	pattern *
	find_pattern (const char *patternstr)
	{
		persistent_ptr<pattern> ptr = patterns;
		while (ptr != nullptr) {
			if (strcmp (patternstr, ptr->get_str ()) == 0)
				return ptr.get ();
			ptr = ptr->get_next ();
		}
		return nullptr;
	}

	pattern *
	create_new_pattern (const char *patternstr)
	{
		pattern *new_pattern;
		transaction::exec_tx (pop, [&] {
			/* allocating new patterns arrray */
			persistent_ptr<pattern> new_patterns
			= make_persistent<pattern> (patternstr);
			/* making the new allocation the actual head */
			new_patterns->set_next (patterns);
			patterns = new_patterns;
			npatterns = npatterns + 1;
			new_pattern = patterns.get ();
		});
		return new_pattern;
	}

	void
	print_patterns (void)
	{
		cout << npatterns << " PATTERNS PROCESSED"<< endl;
		for (size_t i = 0; i < npatterns; i++)
			cout << string (get_pattern (i)->get_str ()) << endl;
	}
}
...

此处显示的是图 1 中图表的代码示例。您也可以看到 libpmemobj 的标头、定义池大小的宏(POOLSIZE)和用于存储开源池的全局变量(pop,您可以将 pop 视作特殊的文件描述符)。请注意如何使用交易保护 root::create_new_pattern()pattern::create_new_file()file::create_new_line()中的所有数据结构修改。在 libpmemobj 的 C++ 绑定中,使用 lambda 函数便捷地实施了交易(使用 lambdas 要求您的编译器至少兼容 C++11)。如果您因为某些原因不喜欢 lambda,可以尝试另一种方法

请注意如何通过 make_persistent<>()(而非常规 malloc()或 C++“新”结构)对所有内存进行分配。

旧版 process_reg_file()的功能被迁移至 file::process_pattern()方法。新版 process_reg_file()实施了用于检查当前文件是否已经进行模式扫描的逻辑(查看文件是否处于当前模式下,并且自上次后并未被修改):

int
process_reg_file (pattern *p, const char *filename, const time_t mtime)
{
        file *f = p->find_file (filename);
        if (f != nullptr && difftime (mtime, f->get_mtime ()) == 0) /* file exists */
                return 0;
        if (f == nullptr) /* file does not exist */
                f = p->create_new_file (filename);
        else /* file exists but it has an old timestamp (modification) */
                f->remove_lines ();
        if (f->process_pattern (p->get_str ()) < 0) {
                cout << "problems processing file "<< filename << endl;
                return -1;
        }
        f->set_mtime (mtime);
        return 0;
}

对其他函数仅实施了一项更改-添加修改时间。例如,process_directory_recursive()现在返回 tuple<string, time_t>的矢量(而不单单返回vector<string>):

int
process_directory_recursive (const char *dirname,
                             vector<tuple<string, time_t>> &files)
{
        path dir_path (dirname);
        directory_iterator it (dir_path), eod;
        BOOST_FOREACH (path const &pa, make_pair (it, eod)) {
                /* full path name */
                string fpname = pa.string ();
                if (is_regular_file (pa)) {
                        files.push_back (
                        tuple<string, time_t> (fpname, last_write_time (pa)));
                } else if (is_directory (pa) && pa.filename () != "."&& pa.filename () != ".."){
                        if (process_directory_recursive (fpname.c_str (), files)
                            < 0)
                                return -1;
                }
        }
        return 0;
}

运行示例

接下来,我们使用“int”和“void”两种模式运行该代码。假设 PMEM 设备(真实设备或 使用 RAM 模拟的设备 )安装在 /mnt/mem:

$ ./pmemgrep /mnt/mem/grep.pool int pmemgrep.cpp
$ ./pmemgrep /mnt/mem/grep.pool void pmemgrep.cpp
$

如果运行没有参数的函数,我们将得到高速缓存模式:

$ ./pmemgrep /mnt/mem/grep.pool
2 PATTERNS PROCESSED
void
int

传输模式时,我们将得到实际的高速缓存结果:

$ ./pmemgrep /mnt/mem/grep.pool void
PATTERN = void
        1 file(s) scanned
###############
FILE = pmemgrep.cpp
###############
*** pattern present in 15 lines ***
80:     get_name (void)
86:     get_nlines (void)
98:     get_next (void)
103:    void
110:    get_mtime (void)
115:    void
121:    void
170:    void
207:    get_next (void)
212:    void
219:    get_str (void)
254:    void
255:    print (void)
326:    void
327:    print_patterns (void)
$
$ ./pmemgrep /mnt/mem/grep.pool int
PATTERN = int
        1 file(s) scanned
###############
FILE = pmemgrep.cpp
###############
*** pattern present in 14 lines ***
137:    int
147:            int ret = 0;
255:    print (void)
327:    print_patterns (void)
337: int
356: int
381: int
395: int
416: int
417: main (int argc, char *argv[])
436:    if (argc == 2) /* No pattern is provided.Print stored patterns and exit
438:            proot->print_patterns ();
444:            if (argc == 3) /* No input is provided.Print data and exit */
445:                    p->print ();
$

当然,我们可以继续将文件添加至现有的模式:

$ ./pmemgrep /mnt/mem/grep.pool void Makefile
$ ./pmemgrep /mnt/mem/grep.pool void
PATTERN = void
        2 file(s) scanned
###############
FILE = Makefile
###############
*** pattern present in 0 lines ***
###############
FILE = pmemgrep.cpp
###############
*** pattern present in 15 lines ***
80:     get_name (void)
86:     get_nlines (void)
98:     get_next (void)
103:    void
110:    get_mtime (void)
115:    void
121:    void
170:    void
207:    get_next (void)
212:    void
219:    get_str (void)
254:    void
255:    print (void)
326:    void
327:    print_patterns (void)

并行持久 Grep

既然我们已经讲到了这里,不添加多线程支持未免太可惜了,尤其是该支持只需添加少量的代码(完整代码可从 pmemgrep_thx/pmemgrep.cpp中获取)。

首先需要添加面向 pthread 和持久互斥体(稍后将详细介绍)的相应标头:

...
#include <libpmemobj++/mutex.hpp>
...
#include <thread>

添加了全新的全局变量,以设置程序中的线程数量,现在接收用于设置线程数量(-nt=number_of_threads)的命令行选项。如果没有明确设置 -nt,将默认使用一个线程:

int num_threads = 1;

接下来,将持久互斥体添加至模式类。使用互斥体同步文件链表的写入(在文件粒度中完成并行化):

class pattern
{
        private:

        persistent_ptr<pattern> next;
        persistent_ptr<char[]> patternstr;
        persistent_ptr<file> files;
        p<size_t> nfiles;
        pmem::obj::mutex pmutex;
        ...

您可能想知道为什么需要互斥体的 pmem::obj版本(为什么不使用 C++ 标准版)。这是因为互斥体存储于 PMEM 中,并且 libpmemobj 需要能在崩溃时重置它。如果不能得到正确恢复,损坏的互斥体将创建一个永久的死锁;您可以参阅使用 libpmemobj 进行同步一文,以了解更多信息。 

虽然将互斥体存储于 PMEM 对关联互斥体和特定的持久数据对象有所帮助,但并不是在所有情况下都有此强制性要求。事实上,在本示例中,易失性内存中的单个标准互斥体变量已足够(因为所有线程一次只能处理一个模式)。我使用持久互斥体是为了显示它的存在。

一旦拥有了互斥体,无论持久与否,我们可以将互斥体传输至 transaction::exec_tx()(最后一个参数),以同步 pattern::create_new_file()中的写入:

transaction::exec_tx (pop,
                             [&] { /* LOCKED TRANSACTION */
                                   /* allocating new files head */
                                   persistent_ptr<file> new_files
                                   = make_persistent<file> (filename);
                                   /* making the new allocation the
                                    * actual head */
                                   new_files->set_next (files);
                                   files = new_files;
                                   nfiles = nfiles + 1;
                                   new_file = files.get ();
                             },
                             pmutex); /* END LOCKED TRANSACTION */

最后一步是调整 process_directory(),以创建与连接线程。已面向线程逻辑创建了一个新函数 process_directory_thread()—(该函数根据线程 ID 拆分任务):

void
process_directory_thread (int id, pattern *p,
                          const vector<tuple<string, time_t>> &files)
{
        size_t files_len = files.size ();
        size_t start = id * (files_len / num_threads);
        size_t end = start + (files_len / num_threads);
        if (id == num_threads - 1)
                end = files_len;
        for (size_t i = start; i < end; i++)
                process_reg_file (p, get<0> (files[i]).c_str (),
                                  get<1> (files[i]));
}

int
process_directory (pattern *p, const char *dirname)
{
        vector<tuple<string, time_t>> files;
        if (process_directory_recursive (dirname, files) < 0)
                return -1;
        /* start threads to split the work */
        thread threads[num_threads];
        for (int i = 0; i < num_threads; i++)
                threads[i] = thread (process_directory_thread, i, p, files);
        /* join threads */
        for (int i = 0; i < num_threads; i++)
                threads[i].join ();
        return 0;
}

总结

本文展示了如何转换简单的 C++ 程序,选取了著名的 UNIX 命令行实用程序的简化版本 grep 作为示例,以利用 PMEM。本文提供了详细的代码,首先描述了易失性版 grep 程序的作用。

然后,使用 libpmemobj(PMDK 中的一款核心库)的 C++ 添加了一个 PMEM 高速缓存,对程序进行了改进。最后,使用线程和 PMEM 感知型同步添加了并行处理(在文件粒度中)。

关于作者

Eduardo Berrocal 于 2017 年 7 月加入英特尔,担任云软件工程师。此前,他在伊利诺斯州芝加哥市的伊利诺理工大学(IIT)获得了计算机科学博士学位。他的博士研究方向主要为(但不限于)数据分析和面向高性能计算的容错。他曾担任过贝尔实验室(诺基亚)的暑期实习生、阿贡国家实验室的研究助理,芝加哥大学的科学程序员和 web 开发人员以及西班牙 CESVIMA 实验室的实习生。

资料来源

  1. 持久内存开发套件(PMDK),http://pmem.io/pmdk/
  2. grep 命令手册页面,https://linux.die.net/man/1/grep
  3. Boost C++ 库集合,http://www.boost.org/
  4. libpmemobj 中类型安全的宏,http://pmem.io/2015/06/11/type-safety-macros.html
  5. 面向 libpmemobj 的 C++ 绑定(第 2 部分)-持久智能指针,http://pmem.io/2016/01/12/cpp-03.html
  6. 面向 libpmemobj 的 C++ 绑定(第 6 部分)-交易,http://pmem.io/2016/05/25/cpp-07.html
  7. 如何模拟持久内存,http://pmem.io/2016/02/22/pm-emulation.html
  8. GitHub 中的示例代码链接

Viewing all articles
Browse latest Browse all 583

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>