最近在公司里的项目做的是性能优化,相关性能调优的经验总结也在前一篇文章里说了。这里再说一说和性能相关的东西。主要针对的是C++类库中常用的一些数据结构,比方说std::string、顺序容器(vector)、关联容器(std::unordered_set、unordered_map)等。

我们拿一道典型的面试题来作为本文分析的切入点。题目是这样的 (problem is from leetcode, Word Ladder):

Given two words (start and end), and a dictionary, find the length of shortest transformation sequence from start to end, such that:

  1. Only one letter can be changed at a time
  2. Each intermediate word must exist in the dictionary

For example,

Given:
start = "hit"
end = "cog"
dict = ["hot","dot","dog","lot","log"]

As one shortest transformation is "hit" -> "hot" -> "dot" -> "dog" -> "cog",
return its length 5.

大意就是有一个转换字典并且指定了两个单词(单词1和单词2),求单词1经过多少次转换后可以变成单词2,中间转换过程中生成的单词必须在转换字典中。

第一眼望过去,很明显是一个最短路径的问题,首先把dict转换成一个图,图的顶点就是单词。如果两个单词的编辑距离为一,那么两个顶点之间就有一条边。

解决这个问题的整个逻辑就可以看成是:

  1. 将单词1加入到数组L中。
  2. 判断数组L是否为空,若空,程序退出。
  3. 遍历数组L中的每一个元素:
    • 如果某个元素等于单词2,那么程序返回数组L遍历次数,并退出。
    • 若找不到单词2,那么将所有和数组L中的顶点编辑距离为1的单词顶点找出来,并将数组L清空。
  4. 将第三步找到的所有顶点重新加入到数组L中。
  5. 重复步骤2

这是一个典型的广搜(BFS)问题(不能用深搜(DFS)来解决,为什么?)。

按照上面的思路我们可以先得到一个解决方案:

typedef pair<string, int>       vertex;                              // first is the vertex name, second is the distance from the start node.
typedef vector<vertex> vertex_collection;
typedef unordered_map<string, vertex_collection> graph; // graph.first: graph name, graph.second adjcent vertexes graph g; void initGraph(unordered_set<string>& dict, const string& start, const string& end)
{
g.clear(); for (auto i = dict.begin(); i != dict.end(); ++i)
{
if (1 == getDistance(start, *i))
{
g[start].push_back(make_pair(*i, INT_MAX));
} if (1 == getDistance(*i, end))
{
g[*i].push_back(make_pair(end, INT_MAX));
} for (auto j = next(i); j != dict.end(); ++j)
{
if (1 == getDistance(*i, *j))
{
g[*i].push_back(make_pair(*j, INT_MAX));
g[*j].push_back(make_pair(*i, INT_MAX));
}
}
}
} int bfs(const string& start, const string& end)
{
unordered_set<string> unused;
deque<vertex> processingQueue; processingQueue.push_back(make_pair(start, 1));
while (!processingQueue.empty())
{
vertex v = processingQueue.front();
processingQueue.pop_front(); if (v.first == end)
{
return v.second;
} vertex_collection& candidates = g[v.first];
for (auto i = candidates.begin(); i != candidates.end(); ++i)
{
vertex& c = *i;
if (unused.find(c.first) == unused.end())
{
c.second = v.second + 1;
processingQueue.push_back(c);
unused.insert(c.first);
}
}
} return 0;
} int ladderLength(string start, string end, unordered_set<string> &dict) {
// Start typing your C/C++ solution below
// DO NOT write int main() function
initGraph(dict, start, end); return bfs(start, end);
}

咋一看,没多大问题。但事实上问题还是挺多的,至少存在下面几个问题

  1. 空间上的浪费。初始化时我们做了g[*i].push_back(make_pair(*j, INT_MAX))和g[*j].push_back(make_pair(*i, INT_MAX)),但是在后续bfs时我们更新的vertex c只影响到了graph g中的一个顶点。这个意思是说,当有两个顶点a、b和c的距离是1时,更新c的操作只会更新两个顶点中某一个顶点的邻接表,因为我们的push_back是按值传递的。其实,就本题目来说,vertex完全不需要使用pair。

  2. 其实,第二个问题更严重。那就是初始化函数initGraph。这是个典型的O(n^2)的复杂度,而后面我们的bfs是O(n+e)的复杂度。所以对于一个有4、5千个顶点的图,在initGraph过程就会非常的耗时!!

要解决第二点问题,我们只需要把bfs改一改,把它改成明显的层次搜索的样子。另外,把initGraph全部丢弃。邻接表我们在bfs的时候动态计算。

int bfs(const string& start, const string& end, unordered_set<string>& dict)
{
unordered_set<string> unused;
vector<vertex> level;
vector<vertex> nextlevel; level.push_back(start);
int length = 0;
while (!level.empty())
{
++length;
for (auto i = level.begin(); i != level.end(); ++i)
{
vertex v = *i; if (v == end)
{
return length;
} for (auto j = dict.begin(); j != dict.end(); ++j)
{
const vertex& c = *j;
if (1 == getDistance(v, c) && unused.find(c) == unused.end())
{
nextlevel.push_back(c);
unused.insert(c);
}
}
} swap(level, nextlevel);
nextlevel.erase(nextlevel.begin(), nextlevel.end());
} return 0;
}

这样就是很明显的层次遍历,并且简化了pair类型的vertex,直接使用string。这里用的比较好的一个地方是swap函数,避免了nextlevel向level拷贝数据的操作。这里必须强调下,swap函数绝对是一个非常重要的函数!

我们很快会发现,这样的解决方案还不足以达到所期望的性能。仅从代码的表面看,我们至少可以发现一处可以优化的地方:

  1. 在大数据的情况下,push_back、insert存在比较大的性能问题,这点是显而易见的。动态的内存分配不是个便宜的事情。这点我们可以通过容器的reserve函数来帮忙解决。

但是,事实告诉我们,使用reserve完全不能解决性能问题。

对于有4、5千个顶点来说,如果他是一个比较稠密的图的话,那么nextlevel.push_back(c)和unused.insert(c)会被执行几千次,在这个执行的过程中最大的问题就是字符串的构造函数会被调用很多次。

如何减少调用的次数呢?最直观的解决方案就是将容器保存的数据改成指针。

这个改动的过程相对来说,就会比较大一点,代码的结构也会看起来比较凌乱。虽然可以解决问题,但是绝对不是最好的方案。基于这个想法的改动,你可以在这里(Word Ladder.cpp)看到,实在是太丑了,就不贴了。

通过这一个简单的例子,我们可以看到,C++里常用的一些数据结构在大数据的情况下,性能表现其实是很一般的。造成这样的结果最主要的原因还是在如何使用上。在C++相对于C提供更多便捷的情况下,如何使用好C++却变得越来越有难度。同时C++本身在性能上也确实和C的差距比较大,所以我们可以看到最新的标准增加了右值引用等语言特性,增加了无序关联容器(unordered_set等)等数据结构。

再议C++的性能的更多相关文章

  1. 再议 js 数字格式之正则表达式

    原文:再议 js 数字格式之正则表达式 前面我们提到到了js的数字格式<浅谈 js 数字格式类型>,之前的<js 正则练习之语法高亮>里也提到了优化数字匹配的正则.不过最近落叶 ...

  2. Python学习之再议row_input

    再议raw_input birth = raw_input('birth: ') if birth < 2000: print '00前' else: print '00后' 运行结果: bir ...

  3. 再议Java中的static关键字

    再议Java中的static关键字 java中的static关键字在很久之前的一篇博文中已经讲到过了,感兴趣的朋友可以参考:<Java中的static关键字解析>. 今天我们再来谈一谈st ...

  4. 再议perl写多线程端口扫描器

    再议perl写多线程端口扫描器 http://blog.csdn.net/sx1989827/article/details/4642179 perl写端口多线程扫描器 http://blog.csd ...

  5. 再议Unity优化

    0x00 前言 在很长一段时间里,Unity项目的开发者的优化指南上基本都会有一条关于使用GetCompnent方法获取组件的条目(例如14年我的这篇博客<深入浅出聊Unity3D项目优化:从D ...

  6. 再议Python协程——从yield到asyncio

    协程,英文名Coroutine.前面介绍Python的多线程,以及用多线程实现并发(参见这篇文章[浅析Python多线程]),今天介绍的协程也是常用的并发手段.本篇主要内容包含:协程的基本概念.协程库 ...

  7. StringBuilder String string.Concat 字符串拼接速度再议

    首先看测试代码: public class StringSpeedTest { "; public string StringAdd(int count) { string str = st ...

  8. 再议 MySQL 回表

    一:回表概述 关于回表的概念网上已经有很多了,这里不过多赘述.下面我们直接放一张图可能更直观说明什么是回表. 图中 非聚集索引也叫二级索引,二级索引本质上也是 一 个 B+ 树结构,与聚集索引(也叫主 ...

  9. 再议C风格变量声明

    NeoRAGEx2002曾经有一篇文章提到这个问题,但是有很多内容并没有包括,例如const和__declspec. 最近我遇到一些这方面的问题,感觉有必要做一个系统性的总结.后来经过一些实验,得出了 ...

随机推荐

  1. 用mysql++读写二进制

    方法1: // mysqlTest.cpp : 定义控制台应用程序的入口点. #include "stdafx.h" #include <mysql++.h> #inc ...

  2. window route 命令

    使用 Route 命令行工具查看并编辑计算机的 IP 路由表.Route 命令和语法如下所示: route [-f] [-p] [Command][Destination] [mask Netmask ...

  3. Linux 打通ssh无密码登录

    像hadoop和spark这类的集群,因为master节点要控制slave节点,以及各节点之间要交互信息,所以需要各节点之间能够互相无密码登录. 通过RSA保存密码, 基本操作如下: Step 1: ...

  4. hibernate的二级缓存

    缓存(Cache): 计算机领域非常通用的概念.它介于应用程序和永久性数据存储源(如硬盘上的文件或者数据库)之间,其作用是降低应用程序直接读写永久性数据存储源的频率,从而提高应用的运行性能.缓存中的数 ...

  5. 关于使用nuget的部分代码

    Install-Package 安装包 -Version 4.3.1 参数指定版本 Uninstall-Package 卸载包 Update-Package 更新包 Get-Package 默认列出本 ...

  6. mac攻略(五) -- 使用brew配置php7开发环境(mac+php+apache+mysql+redis)

    前面介绍过基本的配置,后来我又从网上查找了很多资料,经过不断的摸索,下面做了一个总结,希望能对大家提供些许帮助(Mac版本是sierra)   一.mac系统会自带git,而我们要做的是自己安装git ...

  7. css 打字动画

    使用 css 将文字逐字打出 <h1>css is awesome</h1> 要使<h1>标签里的文字逐字打出,对应的样式如下: h1{ width: 14ch;/ ...

  8. JAVA线程同步辅助类CountDownLatch

    一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 用给定的计数 初始化 CountDownLatch.由于调用了 countDown() 方法,所以在当前计数到达 ...

  9. Python3基础 print 查看一个列表中存储的所有内容

    镇场诗:---大梦谁觉,水月中建博客.百千磨难,才知世事无常.---今持佛语,技术无量愿学.愿尽所学,铸一良心博客.------------------------------------------ ...

  10. 运行时c函数

    // 修改isa,本质就是改变当前对象的类名    object_setClass(self, [XMGKVONotifying_Person class]); // self动态添加关联    // ...