Giter Club home page Giter Club logo

alexiachen.github.io's Introduction

Hi there 👋, I am Alex Chen, Chinese.

Read and write lots of code, clone existing things as exercises, learn deeply. - John Carmack

Most of my programming career has involved finding something neat, writing my own version to understand it & often throwing it away. - Edward Kmett

I program those "clones" like I read papers: change a core part; redesign it. Gain progress or understanding why it is what it is. - Edward Kmett

Github Stats

Anurag's github stats github Stats

alexiachen.github.io's People

Contributors

alexiachen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

alexiachen.github.io's Issues

C++的闭包


title: C++的闭包
date: 2018-11-11 08:58:23
tags:
- C/C++

这篇文章,我们来一步一步通过C++的来构造闭包。当然,在没有GC的语言中,闭包的特性属于半残,不是很完美。不过虽然不完美,但是还是能构造一些高阶的表达方式的。这篇文章不会讲解闭包的概念。

首先我们构造一个可调用lambda函数的一个函数,注意这时候是不支持返回值的:

void call(const std::function<void()>& fn)
{
    try
    {
        fn();
    }
    catch(const std::exception& e)
    {
        // handle exception here
    }
}

void print_line(const std::string& text)
{
    std::cout << text << '\n';
}

call([](){ print_line("called.") });

当然,我们还可以改造成模板的形式,本质是一样的:

template <typename Callable>
void call(const Callable& fn)
{
    fn();
}

但是,以上两种方式都没有返回值,如果要支持返回值必须每个返回值类型都指定,比如:

double call(const std::function<double()>& fn)
{
    return fn();
}

double sum(double a, double b)
{
    return a + b;
}

double result = call([](){ return sum(3.14, 2.71); });

这样太不灵活通用了,非常麻烦。其实我们可以通过闭包来巧妙地来达到我们需要的目的:

void callImpl(const std::function<void()>& fn)
{
    try
    {
        fn();
    }
    catch(const std::exception& e)
    {
        // handle exception here
    }
}

template <typename Fn>
auto call(const Fn& fn) -> decltype(fn())
{
    decltype(fn()) result{};
    auto wrapperFn = [&]() -> void {
        result = fn();
    };

    callImpl(wrapperFn);
    return result;
}

double sum(double a, double b)
{
    return a + b;
}


double result = call([](){ return sum(3.14, 2.71); });

这样lambda函数的返回值类型就不用显式指定了。

但是大家注意到没,到现在虽然call模板函数不用显式指定了,但是还是有返回值的,毕竟最后有一个return result的语句,也就是说,对于没返回值的lambda函数,该模板call函数不支持,还有不完善的地方。导致编译错误。

call([](){ print_line("called."); }); // compile error

当然,通过C++的模板元编程,我还有可以有方法来解决这个不完善的地方:

void callImpl(const std::function<void()>& fn)
{
    try
    {
        fn();
    }
    catch(const std::exception& e)
    {
        // handle exception here
    }
}

template <typename Fn>
auto call(const Fn& fn) -> decltype(fn())
{
   return _call(fn, std::is_same<decltype(fn()), void>());
}

template <typename Fn>
void _call(const Fn& fn, std::true_type)
{
    callImpl([&](){ fn(); });
}

template <typename Fn>
auto _call(const Fn& fn, std::false_type) -> decltype(fn())
{
    decltype(fn()) result{};
    callImpl([&]() { result = fn(); });
    return result;
}

auto result = call([]() { return sum(1,2); }); // return int
auto result = call([]() { return sum(1.2,2.3); }); // return double
call([]() { print_line("called"); }); // return void

以上代码就很完美了,几乎无懈可击。

EOF

多核编程的相关理论及实践


title: 多核编程的相关理论及实践
date: 2018-04-13 12:01:23
tags:
- 多线程编程
- 多核编程

这篇文章主要是看brpc的文档的心得体会吧,brpc算是国内高性能RPC框架中文档写得最好,最有诚意的作品了。就算不看它的源码,看它的文档也能收获很大,干货满满,在这过程中也解答了我以前的困惑。

前言

在开篇之前,我想说点文体风格,由于阅读brpc的文档给了我巨大的收获,所以文体的风格会以问答的方式展开。顺便给出原理解释,还有感受下戈君大神在文档中浸淫多年的软件基础设施工程实践心得体会。我文字理解驾驭不了的地方会选择性地摘抄文档内容。

正文

1. 如何针对写多读少的场景设计一个多线程计数器?

大部分RPC框架,或者其他中间件或多或少都需要有这样的需求,因为这些中间件基本都是主打高性能的,高效利用多核多线程。既然是高性能,都需要为它设计计数器,目的是为了方
便统计各种服务的调用次数,还有其他性能参数(QPS,平均延时),以达到监控服务,调优服务的最终目的。
本着最直观的理解,写下了如下最简单的计数器代码:

int global_count = 0;

//高并发下,多线程调用这个服务,显而易见需要加锁解锁,构成一个临界区
void service()
{
    lock();
    global_count++;
    unlock();
}

以上代码是正确的,global_count在多线程调用的累加下不会出错。但是由于多线程高频率修改global_count,性能可能没你想象的好,因为造成了大量的Race Condition。这时候锁的性能造成了瓶颈。你不得不想方设法的绕开锁的限制。

这里有一点值得注意,C++提供的std::mutex在大部分不压榨性能的场景足够用了,造成std::mutex性能低下的两点,一个是std::mutex的粒度过大,也就是临界区过大限制了并发度。另一个就是频繁的访问导致锁争用,上下文切换非常频繁。

那么如何解决计数器遇到的这个问题呢? 其实最直观的就是避免共享,避免锁争用。用thread local技术来避免减少大量的Cache Bouncing,该技术原理是每个线程修改(写)global_count只维持线程自己的一个变量累加的副本。等到有线程读global_count的时候,就把各个线程的变量副本的结果合并起来,这个速度就会显得慢了,不过因为是写多读少的场景,所以没有问题。

2. Cache Bouncing是为何物?

为了以较低的成本大幅提高性能,现代CPU都有cache。cpu cache已经发展到了三级缓存结构,基本上现在买的个人电脑都是L3结构。其中L1和L2 cache为每个核独有,L3则所有核共享。为了保证所有的核看到正确的内存数据,一个核在写入自己的L1 cache后,CPU会执行Cache一致性算法把对应的Cache Line(一般是64字节)同步到其他核的Cache中。这个过程并不很快,是微秒级的,相比之下写入L1 cache只需要若干纳秒。当很多线程在频繁修改某个共享字段变量时,这个字段所在的Cache Line被不停地同步到不同的核上,就像在核间弹来弹去,这个现象就叫做Cache Bouncing。由于实现cache一致性往往有硬件锁,Cache Bouncing是一种隐式的的全局竞争。

当然Cache一致性算法有多种,其中一种听到的最多的叫MESI协议

3. CAS原子指令为什么会这么难?

多核多线程编程常用锁避免多个线程在修改同一个数据时产生race condition。当锁成为性能瓶颈时,我们又总想试着绕开它,而不可避免地接触了原子指令(用于实现Lock-Free和Wait-Free等数据结构和算法)。但在实践中,用CAS原子指令写出正确的代码是一件非常困难的事,各种概念接踵而来,琢磨不透的race condition、ABA problemmemory barrier很烧脑。(Memory Barrier又叫内存栅栏,内存屏障)。

顾名思义,原子指令是对软件不可再分的指令,比如x.fetch_add(n)指原子地给x加上n,这个指令对软件要么没做,要么完成,不会观察到中间状态。常见的原子指令有:

原子指令 (x均为std::atomic) 作用
x.load() 返回x的值。
x.store(n) 把x设为n,什么都不返回。
x.exchange(n) 把x设为n,返回设定之前的值。
x.compare_exchange_strong(expected_ref, desired) 若x等于expected_ref,则设为desired,返回成功;否则把最新值写入expected_ref,返回失败。
x.compare_exchange_weak(expected_ref, desired) 相比compare_exchange_strong可能有spurious wakeup
x.fetch_add(n), x.fetch_sub(n) 原子地做x += n, x-= n,返回修改之前的值。

你已经可以不用显式加锁用这些指令做原子计数,比如多个线程同时累加一个原子变量,以统计这些线程对一些资源的操作次数。但是,这可能会有两个问题:

  • 这个操作没有你想象地快。
  • 如果你尝试通过看似简单的原子操作控制对一些资源的访问,你的程序有很大几率会crash。

4. 什么情况下多核之间的Cache Line会频繁同步导致Cache Bouncing?

一个核心写入自己的L1 cache是极快的(4 cycles, ~2ns),但当另一个核心读或写同一处内存时,它得确认看到其他核心中对应的cache line。对于软件来说,这个过程是原子的,不能在中间穿插其他代码,只能等待CPU完成一致性同步,这个复杂的硬件算法使得原子操作会变得很慢,竞争激烈时fetch_add会耗费数百纳秒左右。访问被多个线程频繁共享的内存往往是比较慢的。比如像一些场景临界区看着很小,但保护它的spin lock性能不佳,因为spin lock使用的exchange, fetch_add等指令必须等待最新的cache line,看上去只有几条指令,花费若干微秒并不奇怪。

要提高性能,就要避免让CPU频繁同步cache line。这不单和原子指令本身的性能有关,还会影响到程序的整体性能。最有效的解决方法很直白:尽量避免共享。(比如用之前提到过的Thread Local)。

一个相关的编程陷阱是False Sharing:对那些不怎么被修改甚至只读变量的访问,由于同一个cache line中的其他变量被频繁修改,而不得不经常等待cache line同步而显著变慢了(也就是被别的变量拉低的性能)。多线程中的变量尽量按访问规律排列,频繁被其他线程修改的变量要放在独立的Cache Line中。要让一个变量或结构体按Cache Line对齐。

5. 基于CAS原语的原子指令编写的Lock-Free和Wait-Free数据结构和算法性能一定更高吗?

诚然,原子指令基于CAS原语的控制并发的粒度会更小,但是由此带来了更大的复杂性。它能为服务赋予两个重要的概念Lock-FreeWait-Free

当然前者的意思不言自明了,就是无锁编程(算法),后者是无等待编程(算法)。有的人说,那好了,采用Lock-Free的算法,不用加锁了,显然更快了。Wait-Free那更加快了,都不用等待了。当然理论上是这么一回事,但是工程实践上又是一回事。无锁编程采用底层的CAS原语其实上的是硬件粒度的锁,理论上是更加快,因为一些实时系统的关键部分和一些关键的数据结构为了压榨性能,确实是用Lock-Free的算法和Wait-Free算法编写的。但是,值得注意的事情是:

  • lock-free和wait-free必须处理更多更复杂的race condition和ABA problem,完成相同目的的代码比用锁更复杂。代码越多,反而比用小粒度的std::mutex耗时长。

  • 使用mutex的算法变相带“后退”效果。后退(backoff)指出现竞争时尝试另一个途径以临时避免竞争,mutex出现竞争时会使调用者睡眠,使拿到锁的那个线程可以很快地独占完成一系列流程,总体吞吐可能反而高了。

mutex导致低性能往往是因为临界区过大(限制了并发度),或竞争过于激烈(上下文切换开销变得突出)。lock-free/wait-free算法的价值在于其保证了一个或所有线程始终在做有用的事,而不是绝对的高性能。但在一种情况下lock-free和wait-free算法的性能多半更高:就是算法本身可以用少量的CAS原子指令实现。实现锁也是要用原子指令的,当算法本身用一两条指令就能完成的时候,相比额外用锁肯定是更快了。

当然,大部分场景几乎不要用Lock-Free和Wait-Free的算法,一个是因为不需要那么极尽变态地压榨性能。另一个是这两种算法其实很复杂,即使写出来了,从形式化的手段也难以证明算法的正确性。对于越复杂的数据结构越是如此。比如Lock-Free的数据结构目前在boost C++中常见的有Queue, Ring Buffer,Stack这些简单的数据结构。但是如果你搜网络上的论文,就很难见到有Lock-Free的树型数据结构。其一是太复杂了,即使实现并证明正确,但是实测性能可能也没加锁的树形数据结构高。如果这样就舍本逐末了。

6.协程是什么?它为什么不能利用多核?

协程是一种用户态的线程,由用户态的调度器来调度,因为其非常轻量,可以用来部分代替线程从而实现所谓的高并发(看场景),而且由于协程不对应内核中的线程概念,所以用户可以一瞬间创建出成千上万个协程,而不消耗太多性能,它们之前切换可以非常快(100ns-200ns),受缓存一致性的影响很小。但是呢,平常我们所说的协程并不能利用CPU多核,因为是它相当于是N:1的线程库,就是N个协程只跑在一个内核线程上,一个内核线程只会在一个核上运行。

还有一个缺点就是,因为协程无法高效利用多核,代码必须非阻塞,否则所有协程都会被block住,对开发者要求比较苛刻。协程的这种特点使其非常适合写运行时间基本能确定的IO服务器,比如http server,在一些精心调试的场景中,可以达到非常高的吞吐量(实时大流量)。协程的这个特点与Event Loop的单线程异步是类似的,一个callback函数如果需要等待比较长的时间,那么整个Event Loop就被block住了,其他事件得不到及时响应。

所以可以看到了,Node.js之类的适配上协程确实可以达到高并发,高吞吐,这句话本身没问题。但是这是有场景的,Node.js适合处理IO数据密集型实时应用系统,比如Web消息实时推送(知乎的问答提示),Web可视化数据实时展示。Node非常适合如下情况:在响应客户端之前,你预计可能有很高的流量,但所需的服务器端逻辑和处理不一定很多(服务端不能有复杂计算,复杂业务逻辑,复杂事务处理)。

当然有懂一点的人可能会说,可以开启多个Node.js进程(Cluster模块)来以多个内核线程达到利用多核的目的,当然至于这样是不是很方便,很高效就是另一个话题了。但是据我所知,主流大型互联网公司基础框架想高效的利用多核都不会选择这种方案。

7. Go语言中的GoRoutine是协程吗?为什么它却能利用多核?

不是,因为GoRoutine是可以利用多核的,它实质上就是个M:N的线程库(一般M会远大于N),M个GoRoutine对应N个内核线程(当然,这个特性是以语言内建特性的方式暴露出来的,而不是标准库的形式,所以写起来会更自然些)。一个RoRoutine因复杂的计算或者同步阻塞IO等待而block住也不会影响其他GoRoutine。

Goroutine调度器的实现由一种关键的技术:Work-Stealing调度算法。这一种算法的目的是想让GoRoutine更快地被调度到更多的CPU核心上。

8. 单线程Reactor和多线程Reactor模型有什么不同?

以libevent, libev等event-loop库为典型。这个模型一般由一个event dispatcher等待各类事件,待事件发生后原地调用对应的event handler,全部调用完后等待更多事件,故为"loop"。这个模型的实质是把多段逻辑按事件触发顺序交织在一个系统线程中。一个event-loop只能使用一个核,故此类程序要么是IO-bound,要么是每个handler有确定的较短的运行时间(比如http server),否则一个耗时漫长的回调就会卡住整个程序,产生高延时。在实践中这类程序不适合多开发者参与,一个人写了阻塞代码可能就会拖慢其他代码的响应。由于event handler不会同时运行,不太会产生复杂的race condition,一般不需要加锁。此类程序主要靠部署更多进程增加扩展性。Redis就是这么干的。

以boost::asio为典型。一般由一个或多个线程分别运行event dispatcher,待事件发生后把event handler交给一个worker线程执行。 这个模型是单线程reactor的自然扩展,叫多线程Reactor,可以利用多核。由于共用地址空间使得线程间交互变得廉价,worker thread间一般会更及时地均衡负载,而多进程一般依赖更前端的服务(Nginx)来分割流量,一个设计良好的多线程reactor程序往往能比同一台机器上的多个单线程reactor进程更均匀地使用不同核心。不过由于cache一致性的限制,多线程reactor并不能获得线性于核心数的性能,在特定的场景中,粗糙的多线程reactor实现跑在24核上甚至没有精致的单线程reactor实现跑在1个核上快。由于多线程reactor包含多个worker线程,单个event handler阻塞未必会延缓其他handler,所以event handler未必得非阻塞,除非所有的worker线程都被阻塞才会影响到整体进展。事实上,大部分RPC框架都使用了这个模型,且回调中常有阻塞部分,比如同步等待访问下游的RPC返回。

9. N:1线程库和M:N线程库又有什么区别?

之前提到过,平常所说的协程(Coroutine)相当于是一个N:1的线程库。但是这么说并不代表N:1的线程库就是协程,它有另外的名字叫纤程(Fiber)。以GNU Pth, StateThreads等为典型,一般是把N个用户线程映射入一个系统内核线程。同时只运行一个用户线程,调用阻塞函数时才会切换至其他用户线程(有调度器,所以跟Event Loop这点细节上又不大一样)。N:1线程库与单线程reactor在能力上是等价的,但事件回调被替换为了上下文(栈,寄存器,signals),运行回调变成了跳转至上下文。和event loop库一样,单个N:1线程库无法充分发挥多核性能,只适合一些特定的程序。只有一个系统线程对CPU cache较为友好,加上舍弃对signal mask的支持的话,用户线程间的上下文切换可以很快(100~200ns)。N:1线程库的性能一般和event loop库差不多,扩展性也主要靠多进程。

M:N线程库也只是一种实现**思路,Goroutine就是这样的实现。即把M个用户线程映射入N个系统内核线程。M:N线程库可以决定一段代码何时开始在哪运行,并何时结束,相比多线程reactor在调度上具备更多的灵活度。但实现全功能的M:N线程库是困难的,它一直是个活跃的研究话题。我们这里说的M:N线程库特别针对编写网络服务,在这一前提下一些需求可以简化,比如没有时间片抢占,没有(完备的)优先级等。M:N线程库可以在用户态也可以在内核中实现,用户态的实现以新语言为主,比如GHC threads和goroutine,这些语言可以围绕线程库设计全新的关键字并拦截所有相关的API。而在现有语言中的实现往往得修改内核,比如Windows UMS和google SwicthTo(虽然是1:1,但基于它可以实现M:N的效果)。相比N:1线程库,M:N线程库在使用上更类似于系统内核线程,需要用锁或消息传递(消息总线,Actor模型,Vert.x,Akka库等代表)保证代码的线程安全。

win32 进程崩溃时禁止弹出错误对话框


title: win32 进程崩溃时禁止弹出错误对话框
date: 2018-06-15 18:58:23
tags:
- 调试
- Win32

在main函数程序初始化的时候加入以下代码即可:

SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); _set_abort_behavior(0,_WRITE_ABORT_MSG);

这样程序就悄无声息的崩溃了,不然守护进程都不起作用。如果不这样做,弹出错误对话框程序如果不点击关闭或发送错误报告就僵死在那里了,守护进程一直发现进程没挂,就不重启。

参考:

[1] https://stackoverflow.com/questions/9718695/how-can-i-supress-all-error-dialogs-when-a-process-crashes-i-only-want-it-to-cr

[2]
https://stackoverflow.com/questions/1861506/prevent-modal-dialog-on-win32-process-crash

致 獵人 的三千八百九十七情詩


title: 致 獵人 的三千八百九十七情詩
date: 2013-06-21 12:11:56
tags:
- 诗歌

该诗歌非本人原创,是由于在下看到南笙的微博,觉得很好,才记录下来的

我的獵人貧窮但自由

暴風雨來臨之前
他在茅屋煮酒

那是夏天,冬天,還是春天
反正不會是秋天

秋天他在我的眼睛裡

親愛的獵人
你的屋頂有很大一個洞

是為了方便我半夜溜進來偷吻你的睡顏嗎
還是 你放屁時 崩壞的?

當雨滴停留在你的酒杯
當風吹落你的睫毛
當雪趟在你的劍柄

是我在為你寫詩

远程桌面的简要调研报告


title: 远程桌面的简要调研报告
date: 2017-12-01 09:41:15
tags:
- 远程桌面
- VNC协议
- RFB协议

前言

这篇调研报告原本是公司在今年四月份做的调研,当时是总结了篇Word文档,由于我有经常写博客和翻阅博客的习惯,所以发到这里,以方面查阅和回顾。

介绍

远程桌面原本是从Windows 2000 Server开始由微软公司提供的,它的功能是当某台计算机开启了远程桌面服务后我们就可以在网络的另一端控制这台计算机了,通过远程桌面功能我们可以实时的操作这台计算机,在上面安装软件,运行程序,所有的一切都好像是直接在该计算机上操作一样。这就是远程桌面的最大功能。

技术方案

远程桌面控制来源于微软,它可以有多种实现方式,网络上常见的一种就是Virtual Network Computing,也就是通常所说的VNC,VNC是采用RFB(remote frame buffer)协议图形化的桌面共享系统,它可以远程控制其他电脑。VNC在网络上有很多开源实现,比如RealVNCTightVNC。二者都是开源的。其中TightVNC还提供远程桌面客户端(Viewer)的SDK,遗憾的是目前只提供C#的,还有一个Java的Viewer,遗憾的不是SDK,而是整个客户端Viewer。

VNC基本组成架构

VNC总体遵循C/S架构。所以可以归结以下几点:

  • VNC Server, 共享被控机器的屏幕,以被动的方式受VNC Client 控制
  • VNC Client, 有时候也称为VNC Viewer,可以观看服务端控制的屏幕,远程操作服务端
  • VNC协议,准确来说是RFB协议,没有采用该协议的远程桌面就不叫VNC,该协议的目的非常简单,就是把一帧帧地把像素矩阵(坐标系统是以左上角为原点的二维x,y坐标系)和事件消息(鼠标消息,键盘消息)从服务端传送到客户端。

基本原理

VNC Server 运行在被控方的机器不需要物理显示器,一个默认的方式就是,客户端Viewer连接到Server端的端口上(默认5900),当然,浏览器其实也可以连接到Server端(这必须看实现,默认端口5800)。最后,Server也能以“侦听模式”连接客户端上的5500端口,这样的一个优势就是,Server端不需要配置防火墙就能允许客户端连接5900或5800端口,对于客户端来说,服务端配置的人员就可以不需要懂这些知识点,更多的是客户端的操作人员需要懂。

从远程屏幕帧数据流向来讲,Server端是把一帧的frame buffer分解成多个块矩阵发送给客户端,RFB协议可以使用多种带宽,所以这多种的方法,就是为了降低server端和client端的过多的通信核交成本。比如,RTB协议有多种编码类型(为了更加高效的传输这些多个矩阵块),RFB协议允许客户端和Server端在开始传输之前协商好即将使用的编码类型。最简单的编码类型所有客户端和Server端都支持,这种编码类型是将像素数据从左到友按照扫描行(scanline)顺序发送,等待整屏幕的像素数据传送完成时,之后就仅仅只发送屏幕中发生变化的像素部分了。但是这种编码类型有一个限制就是,仅仅只能在屏幕不大幅度更新像素的时候工作良好,一旦屏幕像素发生大幅度更新,所占用的带宽就会很大。(鼠标指针移动,打字就是小幅度的更新,但是看电影,滚动屏幕就是大幅度更新)

RFB协议的限制

  • 远程控制的粘贴板不支持复制粘贴Unicode文本,不能传送任何除Latin-1 character set以外的字符集编码。

  • 因为是基于像素传送的协议,所以从效率上来说,就没有那些采用了更加底层的图形系统的(Linux下的X11或windows下的RDP,RDP协议是Windows自带的远程桌面控制采用的协议)解决方案更高效。

  • 不是为安全设计的协议,传输密码有被嗅探到的可能

参考资料

VNC Html 5客户端(Web Sockets,Canvas):

https://github.com/novnc/noVNC

Libvncserver/client :

https://github.com/LibVNC/libvncserver

另外,需要注意的一点就是libvncserver这个开源VNC框架,还有很多BUG,至少在windows 10上是这样的,所以几个月前向作者提了个issue,目测还是没解决,链接在这里:LibVNC/libvncserver#165

解决Visual Studio调试突然变慢卡死的问题


title: 解决Visual Studio调试突然变慢卡死的问题
date: 2018-06-06 15:58:23
tags:
- 调试
- Win32
- Visual Studio

最开始摸不到头脑,之前还能好好调试的啊,现在VS启动调试器变得巨卡。后来在VS的调试菜单的符号选项里面发现了系统环境变量_NT_SYMBOL_PATH 的值为:srvc:\symbolshttp://msdl.microsoft.com/download/symbols

才想起来以前用WinDbg分析过Dump文件,需要到微软的http连接里面下载windows符号文件,所以就配置了该系统环境变量。

VS也能自动识别这个环境变量,就去下载符号文件去了,所以变得很慢并卡死了,晕。

参考:

[1] https://blog.csdn.net/caichengji1/article/details/77524961

再次理解同步异步和阻塞非阻塞


title: 再次理解同步异步和阻塞非阻塞
date: 2017-07-27 09:30:35
tags:
- IO
- 同步异步
- 阻塞非阻塞

唉,涉及到网络IO和文件IO的时候又把这些概念混淆了。


从编程角度阐释

异步与同步与单线程或多线程都无关,是以任务的执行顺序有关,是更加高层次的抽象概念,下面以线程举例子:

同步(一个任务的开始必须依赖另一个任务的完成)

  • 单线程:
    1 thread: / <- Task 1 -> / |
                               | / <- Task 2 -> / |
                                                  | / <---- Task 3 ----> / |
  • 多线程:
thread A: -> / <- Task 1 -> / |
thread B: ---------------->   | -> / <--- Task 2 ---> / |
thread C: -------------------------------------->       | -> / <- Task 3 -> /

异步(一个任务的开始不必依赖另一个任务的完成)

  • 单线程:
1 thread : / <---------- Task 1 ----------> /
                           / <---- Task 2 ---> /
                   / <---- Task 3 ----> /
  • 多线程:
thread A: -> / <- Task 1 -> /
thread B: -----> / <-------- Task 2 --------> /
thread C: -----------> / <- Task 3 -> /

所以说,单线程也可以异步,多线程也可以同步。在多线程同步中,一个线程需要等待另一个线程的完成,所以会阻塞当前线程(操作系统会挂起当前线程),也就是多线程同步过程中,一般会有阻塞。单线程同步一般就不会有阻塞,所以在Socket编程中,在创建socket的时候,一般有block和non-block选项,系统会为了等待数据阻塞当前等待数据的线程,并由其他读取数据完毕的线程唤醒等待的recv线程,而继续运行。如果是选择non-block,那么等待数据的线程不会被挂起,无论网络数据是否到达会继续recv返回执行,需要调用者不停地轮询数据是否到达,到达再读取。

block和non-block也只是概念,它的底层大部分是由锁来实现的,是锁概念的延伸和高层次的概念映射,比如读写锁,如果写锁被上锁了,那么读操作会被阻塞(读线程被挂起),等到写锁释放,才能继续读。所以,在谈到block和non-block的时候就会涉及到多线程的概念,肯定底层会有多线程的操作。单线程是不可能有阻塞和非阻塞的概念的。

当然,有人说node.js中的readFile和readFileSync就分别是是异步非阻塞和同步阻塞。而node是单线程的为什么会有block和non-block的概念?因为涉及到阻塞和非阻塞一般是底层可能会有多线程处理IO的操作,nodejs的应用层面是看不到的,因为阻塞的线程是不会自动醒来的,必须要另一个线程来唤醒(Resume)以提示IO的完成。也就是nodejs的应用代码确实是单线程执行的,但是在发起文件IO的时候,肯定会有个IO线程来对文件进行读写,最后通知应用层的js代码。其实以上的说法是不准确的,因为对于文件读写是没有非阻塞这以说法的,因为底层肯定会有阻塞操作等待数据读进缓冲区,还是有锁来阻塞。所以在谈及文件IO的时候我们一般只谈及同步和异步,在谈及网络IO的时候只谈及阻塞和非阻塞。

当然我也觉得同步异步和阻塞非阻塞不能单从字面意义上理解。同步和异步关注的是消息通信机制,所谓同步就是caller主动等待callee的结果,也就是说在没有得到调用结果之前,callee就不返回,一旦callee返回了,一定得到返回结果了。异步正好相反,callee被调用之后,caller对于callee的调用就直接返回了,callee通过状态,或者消息,回调函数等机制来把调用的处理结果通知给caller。

阻塞和非阻塞关注的是caller在等待调用结果(返回值,消息等)时的状态,也就是caller当时的状态。阻塞调用是指callee的被调用结果返回之前,当前的caller线程会被挂起,只有得到结果了,callee才会返回,caller线程被唤醒并继续执行。非阻塞调用是指callee不能得到结果之前,对callee的调用不会阻塞caller的当前线程。

据知乎上的陈硕所说,在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步 IO。

怎样理解他这句话呢?《Unix网络编程》这大部经典著作其中有段原文:

POSIX defines these two terms as follows:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes.
  • An asynchronous I/O operation does not cause the requesting process to be blocked.

Using these definitions, the first four I/O models—blocking, nonblocking, I/O multiplexing, and signal-driven I/O—are all synchronous because the actual I/O operation (recvfrom) blocks the process. Only the asynchronous I/O model matches the asynchronous I/O definition.

简单来说就是,对Unix-Like的系统来讲:阻塞式I/O(默认),非阻塞式I/O(nonblock),I/O复用(select/poll/epoll)都属于同步I/O,因为它们在数据由内核空间复制回进程缓冲区时都是阻塞的(不能干别的事)。只有异步I/O模型(AIO)是符合异步I/O操作的含义的,即在1数据准备完成、2由内核空间拷贝回缓冲区后 通知进程,在等待通知的这段时间里可以干别的事。

Reactor模式和Proactor模式

通常我们谈论事件驱动**的时候会提到这两个模式,那么这两个模式到底有什么区别呢?

在Unix标准中的同步IO中的非阻塞IO,也就是NIO(non-block IO),就是基于事件驱动**的,实现上采用Reactor模式,从程序角度而言,当发起IO的读或写操作时,是非阻塞的;当Socket有流可读或可写入Socket时,操作系统会相应地通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。对于网络IO而言,主要有连接建立、流读取及流写入三种事件,Linux 2.6以后的版本采用epoll 方式来实现NIO。

对于另一种异步IO,也就是AIO(asyn IO),同样是基于事件驱动的**,实现上通常采用Proactor模式。从程序角度而言,和NIO不同,当进行读写操作时,只需要直接调用API的read或write方法即可。这两种方法均为异步,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序。对于写操作而言,当操作系统将write方法的流写入完毕时,操作系统主动通知应用程序。与NIO比较而言,AIO一方面简化了程序的编写,流的读取和写入都由操作系统来代替完成,另一方面省去了NIO中程序遍历事件通知队列的代价。windows基于IOCP实现了AIO,对,IOCP就是异步的,但是Linux目前只有基于Epoll模拟实现的AIO。显然,Linux从Epoll到AIO有一层转换,就是用同步的Epoll来模拟异步,Epoll是Reactor模式,AIO是Proactor模式。 所以这两个模式的关系,你可以简单理解为,Proactor模式是同步Reactor模式的异步体现,Proactor模式的层次更高,简化了程序编写。从复杂度来将,Reactor更反人类,因为Reactor是通知程序员有数据可以读写了,程序员再主动读写数据。Proactor模式是数据读写完毕了,再主动通知程序员。

需要注意的一点就是Reactor模式一般是单线程实现,几乎所有的Reactor实现的系统都是单线程的,尽管Reactor模式也可以工作在多线程环境下,因为Proactor模式是Reactor的异步形式的变种,所以Proactor模式一般也是单线程实现的,而非多线程。

对Nginx队列源码ngx_queue_t的一次分析


title: 对Nginx队列源码ngx_queue_t的一次分析
date: 2017-11-18 13:30:00
tags:
- 数据结构与算法
- 双向链表

前言

前几天公司的群里讨论了Nginx内部实现的一个数据结构,ngx_queue_t。 实质上是一个双向链表,但是当时讨论没有讨论出结果来,原因我自己也被绕晕了,今天本着从问题本身出发,不被别人的想法思路所左右,不看网络上任何文章的分析,好好写测试用例来证明自己的想法,其实那天的问题是围绕着两个C语言的宏展开的,那么这篇文章也会重点讲解这两个宏的意义:offsetof 和 ngx_queue_data。所以这篇文章不会涉及到链表插入,删除等细节。

正文

首先,Nginx源码实现这个数据结构,用了大量的宏,我们先用我们的思路,把关键信息抽取出来:

#define u_char unsigned char  
typedef struct ngx_queue_s  ngx_queue_t;

struct ngx_queue_s {
    ngx_queue_t  *prev;   //前一个  
    ngx_queue_t  *next;   //下一个  
};

以上代码就是这个链表的关键,ngx_queue_s 和 ngx_queue_t都是链表的节点类型,结构体中有链表的前驱(prev)和后驱(next),以实现正反向遍历。唉? 等等,如果大学的时候,上过数据结构这门课的人会有点奇怪,会这样想:链表的节点应该有相关的数据啊,不然链表有什么意义? 是的,链表的节点必须有数据,但是上面这个结构体却没有,这是怎么回事?如果在当时,我想大部分人会写成以下这样:

struct ngx_queue_s {
    int data;             // 数据
    ngx_queue_t  *prev;   //前一个  
    ngx_queue_t  *next;   //下一个  
};

这样每访问一个节点的时候,就可以通过节点的指针指向数据了。但是,你有没有想过,万一这个链表的数据是人名呢?不是int型,又或是其他复杂点的类型怎么办? 是不是又要重新定一个新的链表节点类型? 这样就麻烦了不少,软件工程里面奉行了一个原则,这个原则就是DRY原则。所以为了让不同类型采用同一个数据结构实现,就需要类型与数据结构实现分离,让这个数据结构的实现适配不同的类型,当然C++中有STL已经帮你做好了,Java也是。但是C语言就不支持泛型,Nginx由于是C语言实现的,所以采用了一种叫寄宿链表的**,这种**就是分离数据类型与数据结构实现的。

看到这里,就有人会想,这种**跟文章要分析的两个宏有什么关系呢? 现在我可以先放结论,它们之间有莫大的关系,正因为采用了寄宿链表的**,才会间接产生了那两个宏,这种变形掩盖了一些实质。所以其实宏不是关键,是寄宿链表的**才是关键!我们需要知道Why,而不是How,不然会深入无限的细节当中去。

话说回来,下面我们来看那两个宏的实现:

// 取struct_type类型的成员相对于struct_type基地址的偏移量(字节数) 一定要是POD 类型
#define offsetof(struct_type, member)   ((size_t) &((struct_type *) 0)->member)   

#define ngx_queue_data(node_addr,struct_type,member)  (struct_type *)((u_char*)node_addr - offsetof(struct_type, member))

我们发现ngx_queue_data这个宏复用了offsetof这个宏,那么首先分析offsetof这个宏:

这个宏根据名字可以很容易就看出来,就是字面意思,取某个结构体成员相对于结构体基址的偏移量(字节)。我们写一段代码测试来验证我们的想法:

typedef struct _SFamily{
    int id;
    char addr[256];
    int nums;
}Family;

//编译时断言,所谓的静态断言, 所以offsetof这个宏在编译时就计算出了结构体成员的偏移
static_assert(offsetof(Family, nums) == 260,"nums offset is 260");
static_assert(offsetof(Family, id) == 0, "id offset is 0");
static_assert(offsetof(Family, addr) == 4, "addr offset is 4");

根据代码结果,轻而易举的验证了我们的想法,而且这个求偏移是静态编译时就能得出的。不然以上代码直接不能通过编译。

好了,之后我们先别着急研究ngx_queue_data这个宏是怎么回事。先来看看怎么使用这个数据结构,怎样来用操作数据结构的一些方法,Nginx中已经定义好了,但是这些方法,是大量的宏定义的,这些宏不是关键,我把它们都改成了C/C++的函数形式:

#define u_char unsigned char  

// 取struct_type类型的成员相对于struct_type基地址的偏移量(字节数) 一定要是POD 类型
#define offsetof(struct_type, member)   ((size_t) &((struct_type *) 0)->member)   

typedef struct ngx_queue_s  ngx_queue_t;


struct ngx_queue_s {
    ngx_queue_t  *prev;   //前一个  
    ngx_queue_t  *next;   //下一个  
};

void ngx_queue_init(ngx_queue_s* q){
    (q)->prev = q;                                                          
    (q)->next = q;
}

bool ngx_queue_empty(ngx_queue_s* head){
    return head == head->prev;
}

void ngx_queue_insert_head(ngx_queue_s* head, ngx_queue_s* node){
    
    node->next = head->next;
    head->next->prev = node;
    head->next = node;
    node->prev = head;

}

void ngx_queue_insert_after(ngx_queue_s* pos, ngx_queue_s* node){
    ngx_queue_insert_head(pos, node);
}

void ngx_queue_insert_tail(ngx_queue_s* tail, ngx_queue_s* node){

    node->prev = tail->prev;
    tail->prev->next = node;
    tail->prev = node;
    node->next = tail;
}

// 头节点和尾节点都是哑节点(dummy node),并不存储实际数据
ngx_queue_s* ngx_queue_head(ngx_queue_s* head){
    return head->next;
}

ngx_queue_s* ngx_queue_last(ngx_queue_s* tail){
    return tail->prev;
}

//直接取 head dummy node
ngx_queue_s* ngx_queue_sentinel(ngx_queue_s* head){
    return head;
}

//当前节点的下一个节点
ngx_queue_s* ngx_queue_next(ngx_queue_s* cur){
    return cur->next;
}

//当前节点的上一个节点
ngx_queue_s* ngx_queue_prev(ngx_queue_s* cur){
    return cur->prev;
}

//删除节点
void ngx_queue_remove(ngx_queue_s* node){
    node->prev->next = node->next;
    node->next->prev = node->prev;
    
}

#define ngx_queue_data(node_addr,struct_type,member)  (struct_type *)((u_char*)node_addr - offsetof(struct_type, member))

这些函数的用法官方都提供了,我们根据用法,自己实现些测试用例,我们打算实现两个链表,一个是存放书籍信息的,一个是存放人员信息的,写入数据,然后遍历打印它们,看看接口是否正确:

typedef struct _Book{
    ngx_queue_t link;
    int type;
    char name[256];
}Book;

typedef struct _Person{
   
    int sex;
    char name[256];
    ngx_queue_t link;
}Person;

 Book book1, book2, book3;
    Person person1, person2, person3;

    static_assert(offsetof(Book, link) == 0, "link offset is 0");
    static_assert(offsetof(Person, link) == 260, "link offset is 260");

    book1.type = 1;
    book2.type = 2;
    book3.type = 3;

    person1.sex = 1;
    person2.sex = 0;
    person3.sex = 1;

    strcpy(book1.name, "Harry Potter");
    strcpy(book2.name, "the lord of ring");
    strcpy(book3.name, "Jame");

    strcpy(person1.name, "Fan BingBing");
    strcpy(person2.name, "Wu YanZu");
    strcpy(person3.name, "ShuQi");

    //初始化队列
    ngx_queue_t bookQueue;
    ngx_queue_init(&bookQueue);
    ngx_queue_insert_head(&bookQueue, &book1.link);
    ngx_queue_insert_tail(&bookQueue, &book2.link);
    ngx_queue_insert_tail(&bookQueue, &book3.link);

    ngx_queue_t personQueue;
    ngx_queue_init(&personQueue);
    ngx_queue_insert_head(&personQueue, &person1.link);
    ngx_queue_insert_tail(&personQueue, &person2.link);
    ngx_queue_insert_tail(&personQueue, &person3.link);

   
    for (auto iter = ngx_queue_head(&bookQueue); iter != ngx_queue_sentinel(&bookQueue); iter = ngx_queue_next(iter))
    {
        Book* bookPtr = (Book*)ngx_queue_data(iter, Book, link);

        printf("Book name is: %s . type is %d \n", bookPtr->name, bookPtr->type);
    }

    for (auto iter = ngx_queue_head(&personQueue); iter != ngx_queue_sentinel(&personQueue); iter = ngx_queue_next(iter))
    {
        Person* personPtr = (Person*)ngx_queue_data(iter, Person, link);

        printf("Person name is: %s . sex is %s\n", personPtr->name, personPtr->sex == 1 ? "Female" : "Male");
    }

以上代码的打印结果如我们所想,是正确的。注意了,我们遍历访问链表单个节点的时候用了ngx_queue_data这个宏,正是我们需要了解的宏。看来跟C语言数组,C++ vector的遍历访问差不多嘛,访问一个节点元素需要的就是节点的下标(或者说地址)。

接下来我们再看链表节点的真正的数据是定义在哪?

typedef struct _Book{
    ngx_queue_t link;
    int type;
    char name[256];
}Book;

typedef struct _Person{
   
    int sex;
    char name[256];
    ngx_queue_t link;
}Person;

注意了,以上两个结构体里面都有链表节点类型ngx_queue_t,变量名link,想想,是不是数据类型与数据结构就分离了?
不同的数据类型复用了相同类型的数据结构? 这样访问节点就可以通过它们的link成员来正反向迭代了,所以之前的代码iter变量实际就是link成员的地址,我们可以访问每个结构体里面的link成员变量了,那么怎么知道结构体其他数据呢?没有地址,不知道在哪啊? 其实关键就是通过link成员的地址来计算出结构体的地址的,下面举个例子:

// iter为链表节点的地址,而链表节点又在Book和Person结构体中
Book* bookPtr = (Book*)ngx_queue_data(iter, Book, link);

Person* personPtr = (Person*)ngx_queue_data(iter, Person, link);

看到上面代码并且联系ngx_queue_data宏的实现,是不是就明白了?

链表节点成员的地址 ➖ 链表节点成员相对于结构体基址的偏移 = 结构体的基址

那么是不是就可以通过结构体基址的地址强制转换为结构体的指针,访问结构体其他数据成员了?还不明白? 那么再看下面:

typedef struct _Book{
    ngx_queue_t link; // 这个link变量的地址知道了,是不是就可以知道Book结构体基址了?  link的地址假如为123,link的偏移为0,那么123 - 0 = 123。123为数据的结构体地址
    int type;
    char name[256];
}Book;

typedef struct _Person{
   
    int sex;
    char name[256];
    ngx_queue_t link;// 这个link变量的地址知道了,是不是就可以知道Person结构体基址了?  link的地址假如为300,link的偏移为260,那么300 - 260 = 40。40为数据的结构体地址
}Person;

EOF

C++最佳实践


title: C++最佳实践
date: 2017-02-22 08:58:23
tags:
- C/C++
- 软件工程

该篇文章是我自己的经验总结,不可能100%适合读者,当然相关的C++工程实践书籍类似《Effective C++》,《More Effective C++》,《Modern Effective C++》,《Learning C++ Best Practices》 《Google C++ Coding Style》等等可能都有类似描述,我这篇文章可能也是从以上的书籍文章汲取了一些。

工欲善其事必先利其器

现在软件工程越来越发达,C++的标准也一直在改进更新,这门在大众来看的“古老”语言也在慢慢变得更加像现代编程语言一样了,现代软件工程持续交付,持续集成等工具概念层出不穷,这里我想推荐些工具给读者改进优化项目开发流程

  • Cmake,最好的C++跨平台构建工具,没有之一,automake,qmake在它面前黯然失色。

  • Travis CI, 这个是持续集成工具,能在Github上很好的工作。

  • CppCheck, C/C++ 静态分析工具,免费的,能查出很多类型缺陷,内存泄漏和资源泄漏。当然还有很多语言的静态分析工具,如果有兴趣,请看这里

另外,在个人项目和公司项目中,C++编译器,无论在g++,MSVC,或者clang上,请把警告级别调整到最高。MSVC我是调整到W4级别,g++上,由于本人不熟悉g++的警告类型,那么请开-Wall -Wextra警告并严格观察,另外g++上还可以开-Weffc++选项,编译器会按照《Effective C++》的实践规范来检查代码的隐患。这些都是很重要,编译器的警告很重要!!要好好利用静态类型语言带来的优点。最后把警告尽量消除到0 warnings为止! 最后的最后,用C++的静态分析工具检查一遍所有的源码,选择性的消除工具报告出来的缺陷。这样你会发现,后期的软件的运行时的BUG会少很多,特别是不明不白的crash。

正文

1.基本C++命名规范

  • 类名用驼峰命名法: MyClass
  • 类的成员函数和变量名开头单词用小写:myMethod
  • 常量全用大写:const double PI = 3.1415926

另外C++标准库和Boost采用另一种规范,如果你的代码与标准库和Boost混合写契合度很高,推荐用以下的规范:

  • 宏名称单词全用大写,单词之间用下划线隔开: INT_MAX
  • 模版参数使用驼峰命名法: InputInterator
  • 其他所有变量和函数名,类名全用小写单词加下划线隔开:make_shared,unordered_map,dynamic_cast

2.区分私有成员变量

  • 在私有成员变量前面加入m_前缀, m代表“member”: m_height

当然,个别一些习惯,是在私有成员变量后加下划线后缀: object_

3.区分函数参数

  • 在函数参数名加入t_前缀: t_height

当然,代码最重要的还是要与CodeBase一致,最终看公司的规范,这里只是一个样例,t可以认为是“the”的缩写。这只是区分函数参数与局部变量的一种策略。

4.任何命名不能是下划线开头

如果你这么做,那么可能会与编译器的扩展关键字造成冲突。如果好奇,那么请看stackoverflow的这个讨论

5.一个良好的样例

class MyClass
{
public:
  MyClass(int t_data, int t_attr)
    : m_data(t_data),m_attr(t_attr)
  {
  }

  int getData() const
  {
    return m_data;
  }

  int attribute() const
  {
      return m_attr;
  }

private:
  int m_data;
  int m_attr;
};

6.空指针的表示请用nullptr

C++ 11 中的空指针是一个特定的值,用以代替0或NULL。 如果好奇,请看知乎的讨论, 不然值会有二义性。另外,知乎上还有讨论2

7.注释

优先使用//来注释代码块,不要使用/**/

8.不要在头文件中使用using namespace

这会导致using的名字空间污染范围扩散,因为使用了这个头文件的源文件都隐式使用这个名字空间了,这将来容易造成名字空间的冲突,该错误查找困难。不利于后来开发人员维护。

9.头文件守护

这个想必很多人已经习惯了,不过如果不这样做的危害还是要说一下,这样可以防止头文件被重复包含多次而造成的问题,也能解决意外包含其他工程头文件的冲突。

#ifndef MYPROJECT_MYCLASS_HPP
#define MYPROJECT_MYCLASS_HPP

namespace MyProject {
  class MyClass {
  };
}

#endif

10.代码块一定要用{}

如果不这么做,可能会导致一些语义错误。

// Bad Idea
// 这么做虽然没错,能按照预想运行,但是会给后来人员造成迷惑
for (int i = 0; i < 15; ++i)
  std::cout << i << std::endl;

// Bad Idea
// 这就有错了,std::cout没在循环内,变量i也不是循环内的,与预想不一致
int sum = 0;
for (int i = 0; i < 15; ++i)
  ++sum;
  std::cout << i << std::endl;


// Good Idea
// 这个语义就完全正确了。
int sum = 0;
for (int i = 0; i < 15; ++i) {
  ++sum;
  std::cout << i << std::endl;
}

11.限制代码列的字符数

一般推荐是80-100个字符之间,我自己是80。一般IDE和文本编辑器都可以强制限制。

// Bad Idea
// 难阅读
if (x && y && myFunctionThatReturnsBool() && caseNumber3 && (15 > 12 || 2 < 3)) {
}

// Good Idea
// 逻辑思路跟得上了,容易阅读
if (x && y && myFunctionThatReturnsBool()
    && caseNumber3
    && (15 > 12 || 2 < 3)) {
}

12.使用""包含本地头文件

<> 是保留给标准库和系统库头文件的,自己写的本地头文件#include "MyHeader.h"

13.初始化成员变量

最好用初始化成员列表来初始化。

// Bad Idea
class MyClass
{
public:
  MyClass(int t_value)
  {
    m_value = t_value; //这是赋值,而不是初始化
  }

private:
  int m_value;
};


// Good Idea
// C++ 初始化成员列表是C++语言特有的,这样写代码更加清晰干净,
// 而且还有潜在的性能提升,因为初始化和赋值不是一个概念。
// 《Effective C++》也提到过
class MyClass
{
public:
  MyClass(int t_value)
    : m_value(t_value)
  {
  }

private:
  int m_value;
};

好奇请戳知乎的讨论

当然,在C++ 11中,你可以考虑总是给成员变量一个默认值,

// ... //
private:
  int m_value = 0;
// ... //

使用大括号初始化,因为它在编译时不允许数据收窄。

// Best Idea

// ... //
private:
  int m_value{ 0 }; // allowed
  unsigned m_value_2 { -1 }; // compile-time error, narrowing from signed to unsigned.
// ... //

优先使用大括号初始化,除非有原因的特殊要求不那么做。

14.总是使用名字空间

在C语言时代,很多库的开发者,为了防止函数符号链接时冲突,就在函数名加入库名称的前缀,比如OpenCV的函数都是cv_xxxx。当然,这是历史原因,如果是采用C++ 编译器,就应该使用namespace防止符号冲突,采用boost库的方式。

15.使用标准库提供的正确的整型类型

在C++中,最好不要出现int类型,最好是intxxx_t , uintxxx_t。 表示大小请使用std::size_t。

可以看这里的参考,这样提高可移植性,因为在不同类型的平台上,这些类型会typedef到特定类型上去。

注意: signed char 保证至少 8 位,int 保证至少 16 位,long 保证至少 32 位,long long 保证至少 64 位。

如果还不明白,去看stackoverflow的讨论

16.Tab和空格不要混合使用

这个绝对禁止,应该从编辑器和IDE里面更改设置,比如让Tab等于4个空格。至于设置Tab等于多少个空格合理,这个是个人喜好问题,不然就是Emacs和Vim之争了。

17.不要害怕模版

这个,我对于C++的模版元编程不熟悉,就不做过多讨论了。模版可以说是另外一种语言,另一种“函数式”语言。它是图灵完备(Turing-Complete)的。

// check specific class if it has foo() function
template <typename Ty>
class has_foo_function {

private:
	typedef char yes[1];
	typedef char no[2];
	
	template <typename Inner>
	static yes& test(Inner *I, decltype(I->foo()) * = nullptr);
	
	template <typename>
	static no& test(...);
	
public:
	static const bool value =
		sizeof(test<Ty>(nullptr)) == sizeof(yes);
};

class MyTest1
{
public:
	void foo(){};
	
};

class MyTest2
{
public:
	
};

class MyTest3
{
public:
	int foo(int s){ return s; }
};

int main()
{
  std::cout << has_foo_function<MyTest1>::value << std::endl; // 1
	std::cout << has_foo_function<MyTest2>::value << std::endl; // 0
	std::cout << has_foo_function<MyTest3>::value << std::endl; // 0
  return 0;
}

感兴趣可以在网络上看到各种玩法:

18.慎用操作符重载

重载操作符是个很方便的特性,比如用C++ 实现一个BigInteger类,一个类的实体表示一个大整数,如果给类实现一个add函数表示大整数做加法,a.add(b),那么太不直观了,a + b更加方便直观。另一个官方例子就是std::string重载了+号操作符,以便字符串的拼接。当然,重载操作符本质上还是调用函数。

虽然方便,但是如果随意重载操作符,可能导致许多诡异的错误,详情见这里的讨论

尤其是,当需要编写重载操作符的时候,应该把以下几点时时刻刻记住在脑子里:

  • 当有必须的理由的时候才重载 operator=
  • 对于剩下的全部操作符,也是有需要在一个场景下使用的时候才重载它们,这个场景比如,+ - 是一对语义上有关联的符号
  • 时刻注意操作符的优先级,即使重载了,那操作符的优先级也不会改变,不能凭感觉猜测。
  • 不要重载一些特殊奇怪的操作符,比如 ~ 和 % #之类的,除非有理由让你这么做
  • 永远不要重载逗号(,)操作符

剩下给出一个重载操作符的参考资料

19.消除隐式转换

  • 对于单参构造函数,必须在构造函数前加入 explicit 关键字,要求显式调用
  • 对于操作符转换也是一样的,需要加 explicit 关键字
//bad idea
struct S {
  operator int() {
    return 2;
  }
};

//good idea
struct S {
  explicit operator int() {
    return 2;
  }
};

20.考虑零原则(Rule of Zero)

该原则声称,不要提供任何编译器能自动生成的函数(拷贝构造,拷贝赋值构造,移动构造,移动赋值构造,析构函数),除非该类需要构造一些关于所有权(ownership)的概念。

该原则的目的是让当更多的成员变量被添加到该类时,让编译器提供最佳的版本,换句话说,这些东西最好让编译器来维护。

该原则的背景在这里,然后这篇文章做了实现并解释了技术。

21.尽可能多的用const修饰

const 告诉编译器方法或变量是不可变(immutable)的,这也能帮助编译器优化代码,同时也能帮助开发者知道函数是否有副作用(side-effect)。另外,常引用(const &)也能阻止编译器进行不必要的拷贝代价。感兴趣的可以看这篇文章底下John Carmack的评论

// If the parameter is input parameter, not output
// Bad Idea
class MyClass
{
public:
  void do_something(int i);
  void do_something(std::string str);
};


// Good Idea
class MyClass
{
public:
  void do_something(const int i);
  void do_something(const std::string &str);
};

22.小心函数返回的类型

Getters:

  • 以引用返回或者常引用返回的,在观察者模式下这会有一定的性能提升。
  • 以值返回更加有助于线程安全(thread safety),即使有拷贝的代价,由于RVO的作用,性能也不会损失。

临时和局部变量:

  • 总是以值返回

23.不要用常引用(const &)来传递或返回一个基本类型变量

// Very Bad Idea
class MyClass
{
public:
  explicit MyClass(const int& t_int_value)
    : m_int_value(t_int_value)
  {
  }

  const int& get_int_value() const
  {
    return m_int_value;
  }

private:
  int m_int_value;
}

// Good Idea
class MyClass
{
public:
  explicit MyClass(const int t_int_value)
    : m_int_value(t_int_value)
  {
  }

  int get_int_value() const
  {
    return m_int_value;
  }

private:
  int m_int_value;
}

为什么呢? 因为对于基本类型变量用引用传递或返回会导致指针操作,这会更加慢。如果通过值传递,是利用处理器寄存器直接传递的,会更加快。

24.避免裸指针访问内存

直接内存分配和释放在C++中做到完全消除内存错误和内存泄漏是很难的,要善于利用RAII,C++ 11提供了智能指针这种方便的工具,基本可能杜绝内存泄漏。

// Bad Idea
MyClass *myobj = new MyClass;

// ...
delete myobj;


// Good Idea
auto myobj = std::make_unique<MyClass>(constructor_param1, constructor_param2); // C++14
auto myobj = std::unique_ptr<MyClass>(new MyClass(constructor_param1, constructor_param2)); // C++11
auto mybuffer = std::make_unique<char[]>(length); // C++14
auto mybuffer = std::unique_ptr<char[]>(new char[length]); // C++11

// or for reference counted objects
auto myobj = std::make_shared<MyClass>(); 

// ...
// myobj is automatically freed for you whenever it is no longer used.

25.用std::array 或 std::vector 代替C风格的数组

这两个都使用内存连续分配的,可以完全代替C数组。如果想获得最原生C数组的性能,可以用std::array,它还可以结合STL的算法,非常方便。另外,避免使用std::shared_ptr来指向一个数组,用std::unique_ptr代替。

26.使用C++风格的转换代替C风格的转换

使用C++风格的转换(static_cast<> dynamic_cast<>)代替C风格的转换,C++风格的转换编译器会加入更多的检查,类型更加安全。

// Bad Idea
double x = getX();
int i = (int) x;

// Not a Bad Idea
int i = static_cast<int>(x);

27.不要定义可变参函数

类似printf,可变参的函数不是类型安全的,错误的输入可能导致异常终止,或者让程序产生未定义行为,未定义行为会导致软件的安全问题,如果你有支持C++ 11的编译器,请使用变参模版来代替。

附加内容: 如何防止下一个类似HeartBleed的漏洞

28.避免使用宏

宏是预处理器(preprocessor)做的事,这个事件在编译器开始编译代码之前,这会让编译器少做了类型检查,因为宏本质是个文本替换,如果出现BUG,调试器很难找出问题源头所在。

// Bad Idea
#define PI 3.14159;

// Good Idea
// 类似于Java,Java全局常量也必须定义在类中
namespace my_project {
  class Constants {
  public:
    // 如果上面这段宏被展开了,那么实际会变成下面这行:
    //   static const double 3.14159 = 3.14159;
    //这会导致编译出错. 有些时候,这样的错误很难理解.
    static const double PI = 3.14159;
  };
}

29.函数参数尽量避免bool类型

这对于阅读代码来说,不会增加更多有意义的信息,有了bool变量,就说明函数的输出依赖了这个条件,导致做了过多的事情,一个函数尽量保证只做一件事并做好一件事。如果遇到需要这样写的函数,就把bool的条件分离出一个单独的函数来处理,或者传入枚举类型,让参数更有意义。

这篇文章讲述了更多的细节,以及如何解决。

30.尽量避免裸循环

使用C++ 新标准提供的更加高级的语义。

std::vector<DeviceCollect::st_port> tcp_ports;

// Very Bad Idea
for(int i = 0; i < tcp_ports.size(); ++i)
{
  //do_something
  tcp_ports[i];
}

// range for , foreach semantics
// Not a Bad Idea
for(const auto & prot : tcp_ports)
{
    //do_something
    port;
}


// Very Good Idea
tcp_ports.erase(
		std::remove_if(std::begin(tcp_ports), std::end(tcp_ports),
		[](const DeviceCollect::st_port & o) -> bool { return o.processPath.compare(std::string("")) == 0; }),
		std::end(tcp_ports));

31.正确使用关键字override和final

这些关键字能帮助开发者更清晰的理解虚函数是怎样被使用的,如果虚函数的函数签名有变化,override关键字也能捕获一些潜在的错误,导致编译出错。也能提示编译器如何更好的优化虚函数。

32.了解使用的类型

这个在条款15也提到过,这里会给出一个更详细的参考

33.避免全局数据

全局数据会导致函数间的无法意料的副作用,也会导致代码间难以并行化,或者不可能并行化。

34.避免静态变量

除了全局数据,静态数据的构造和析构可能不总是能按照你预想的进行。如果你的程序是跨平台的,那么这个噩梦可能成真,请看这么个例子,一个g++的BUG引出了共享静态数据析构顺序的一个问题。这个共享静态数据是从动态库中加载的。

35.shared_ptr

该指针几乎等同于全局变量,因为它可以让不同的代码访问相同的数据。

36.单例模式

单例模式一般都需要保证线程安全,因为单例一般实现是static变量和shared_ptr实现的。

37.互斥量和可变量要一起使用

  • 一个可变成员变量如果会被共享就需要被互斥量同步(mutex),或者把可变量做成原子的(atomic)
  • 如果一个成员变量本身是一个互斥量,它也是可变的,要使用它,必须在const 函数中使用

更多详情,请看这里

38.需要的时候就前置声明

用这段代码:

// some header file
class MyClass;
class MyClassB

void doSomething(const MyClass &);
void fuck(MyClassB *);

代替以下代码:

// some header file
#include "MyClass.hpp"
#include "MyClassB.hpp"

void doSomething(const MyClass &);
void fuck(MyClassB *);

如果类是模版类,可以这么写:

template<typename T> class MyTemplatedType;

这样在编译器重新构建依赖的时候,可以有效降低编译时间。

39.避免不必要的模版实例化

模版实例化不是没有代价的,过多的模版实例化,每种类型实例化为一份代码,那么会造成编译出的机器码膨胀,也会加大编译时间。

更多详情,请看这篇文章

40.避免递归模版实例化

递归会让模版实例化瞬间变多,加重编译器负担,造成更加难以理解的代码。

问题解决请看这篇文章

41.不要包含无用的头文件

降低编译依赖,减少编译时间。

42.使用初始化列表

// This
std::vector<ModelObject> mos{mo1, mo2};

// -or-
auto mos = std::vector<ModelObject>{mo1, mo2};

// Don't do this
std::vector<ModelObject> mos;
mos.push_back(mo1);
mos.push_back(mo2);

初始化列表能带来更大性能的提升,减少对象的拷贝和容器的resize

43.使用move语义

move语义是C++ 11的最大革新了。

相关文章:

对于大多数代码,只要这样就可以了:

ModelObject(ModelObject &&) = default;

但是MSVC2013不支持,MSVC2015支持。

44.减少shared_ptr的拷贝

该智能指针允许共享,类似实现Java中的引用计数来保证无资源泄漏,但是拷贝的代价很高,yi你用计数必须是原子的和线程安全的。

45.尽可能降低拷贝或重新赋值

// Bad Idea
std::string somevalue;

if (caseA) {
  somevalue = "Value A";
} else {
  somevalue = "Value B";
}

// Better Idea
const std::string somevalue = caseA ? "Value A" : "Value B";

更加复杂的场景可以这样:

// Bad Idea
std::string somevalue;

if (caseA) {
  somevalue = "Value A";
} else if(caseB) {
  somevalue = "Value B";
} else {
  somevalue = "Value C";
}

// Better Idea
// 这类似与javascript中的匿名函数立即调用
// (function (){ return "hello" })()
const std::string somevalue = [&](){
    if (caseA) {
      return "Value A";
    } else if (caseB) {
      return "Value B";
    } else {
      return "Value C";
    }
  }();

IIFE in C++ 11 请看这里

46.不要大量使用异常

这个《Google C++ Coding Style》上有说,这里就不多做解释了。

47.避免使用new

std::shared_ptr<ModelObject_Impl>(new ModelObject_Impl());

// should become
std::make_shared<ModelObject_Impl>(); // (it's also more readable and concise)

48. 优先使用unique_ptr

如果能用unique_ptr解决的场景,就不要用shared_ptr。

当前的最佳实践就是,从一个工厂函数中返回一个unique_ptr,如果有需要,再从unique转成shared。

std::unique_ptr<ModelObject_Impl> factory();

auto shared = std::shared_ptr<ModelObject_Impl>(factory());

49.限制变量作用范围

变量应该尽可能的推迟声明,什么时候需要用到再声明,不能像C89那样,全部放在函数开头。这样也影响阅读性。降低变量作用范围同时降低内存的使用,帮助编译器生成更高效的代码。

// Good Idea
for (int i = 0; i < 15; ++i)
{
  MyObject obj(i);
  // do something with obj
}

// Bad Idea
MyObject obj; // 无意义的对象初始化
for (int i = 0; i < 15; ++i)
{
  obj = MyObject(i); // 无必要的赋值操作
  // do something with obj
}
// obj还继续占用内存,直到离开它自身的作用范围

50.优先使用double,而不是float

当然了,这也得看场景和编译器的优化水平了,double可能比float更高效。
使用float,因为精度更低,所以在转换中可能更低效。当然,如果在向量化操作中,如果你牺牲精度而换取高性能,float可能会更加高效。

double是C++浮点数的默认类型

float a = 3.14f; // 3.14f默认double,会有一层隐式向低精度转换
double b = 3.14f;  // 由于默认是double 无需转换

51.优先使用++i而不是i++ (可以废弃)

当然,现代编译器优化水平相当高了,在for循环中,两者优化出来的机器码都是一样的,但是你无法保证在个别的编译器平台上能这样。

52. char是char,string是string

// Bad Idea
std::cout << someThing() << "\n";

// Good Idea
std::cout << someThing() << '\n';

前者会被编译器解析成const char*, 当写入流的时候,会检查末尾'\0'结束符。后者被解释成单个字符,省去了CPU很多操作。

如果在特别场景下大量使用前者,可能前者会慢慢变成性能瓶颈。

53.不要使用std::bind

std::bind 在lambda表达式出现之前可以用,但是之后就没有多大用了。大多数情况下,可以用lambda表达式代替。

// Bad Idea
auto f = std::bind(&my_function, "hello", std::placeholders::_1);
f("world");

// Good Idea
auto f = [](const std::string &s) { return my_function("hello", s); };
f("world");

54.善用脚本,拥抱其他语言和框架

静态类型语言与动态类型语言结合才更强劲,参考使用boost::python。
该用什么语言完成的工作就用那种语言完成。写Web就用Java拉,前端就javascript啦,不要折腾没用的。

当你每次鄙视一种语言的时候,就失去了一次向它学习的机会。

从底层角度看待函数的调用


title: 从底层角度看待函数的调用
date: 2014-03-05 12:01:23
tags:
- 内存模型
- C/C++
- 汇编语言

可能分析的时候会用到一点汇编语言,不过都很简单,不影响理解文章。

预备知识

在开始正文之前,需要复习下函数调用的约定。

  • __cdecl: C/C++函数默认调用约定,参数依次从右向左传递,并压入堆栈,最后由调用函数清空堆栈,这种方式适用于传递参数个数可变的被调用函数,只有被调用函数才知道它传递了多少个参数给被调用函数,比如printf();

  • __stdcall:参数由右向左传递,并压入堆栈,由被调用函数清空堆栈,当函数有可变参数个数时,函数调用约定自动转换成__cdecl调用约定;

  • __thiscall:C++非静态成员函数默认调用约定,不能使用个数可变参数,调用非静态成员函数时,this指针直接保存在ecx寄存器中,不入栈,其他方面同__stdcall;

  • __fastcall:凡是接口函数都必须指明其调用规范,除非接口函数是类的非静态成员函数;

简洁的内存模型抽象

由于是简单而本质的抽象,因此我们不考虑分页机制、MMU(memory management unit)之类的。正是如此,它们本来对于我们就是透明的。

所以内存就被考虑为一个从编号(地址)0开始、以编号(地址)0xffff ffff结束的字节序列。每一个字节都被顺序地编号。编号就是字节的地址。
在32位FLAT模式汇编中,本来就是如此。

在程序加载入内存后,程序的指令和数据都按某种方式存放在内存里面。要访问和执行他们,只需要知道他们的地址就可以了。

最重要的东西登场,它就是eip,指令指针寄存器,或称程序计数器。eip中的值程序员无法修改(嗯,可是汇编程序员呢?汇编程序员也无法修改它的值吗?废话,汇编程序员也是程序员啊!),它的值就是下一条即将执行的指令的地址。就是说eip永远指向下一条指令。

然后就是esp,它指向栈的栈顶。当向栈压入数据或从栈弹出数据时,esp的值不断变化,但无论如何变化,它都指向栈顶。

最后就是ebp,它用来把栈中的某个地址作为基址(基本地址,这样理解就是了),它用来标识栈中的某个固定位置,因此可以通过它访问这个固定位置附近的数据。

80X86的栈是向下增长的。也就是说,当向栈压入4个字节的数据时,esp = esp - 4; 当从栈中弹出4个字节时,esp = esp + 4。

栈帧与函数调用

关于计算机,最重要的三个抽象是什么?答案是虚拟地址空间、进程、文件。

一个进程就是一个运行中的程序,或者被加载到内存中的程序。现代操作系统使进程看上去独占了所有的系统资源,但实际上系统中运行着多个进程。

所以从一个进程的视角看去,它独占了系统中的所有内存资源和CPU资源。对于32位系统虚拟地址空间被抽象为编号0~0xffff ffff的字节序列,它是平坦的,线性的,被系统抽象了的,所以叫它平坦地址或线性地址、虚拟地址。

对于Linux来说,保留高1G为系统使用。0-3G空间被应用程序也就是进程独占。

对于一个被加载了的程序也就是进程,其在内存中的分布为:

共享内存段
自由存储区(堆)
BSS段
数据段
只读数据段
代码段

栈向下增长。

每一个函数调用,都是一个栈帧(stack frame)。
以下代码:

int add(int x, int y)
{
    int z;
    z = x + y;
    return z;
}

int main(int argc, char* argv[])
{
    add(3, 5);
    return 0;
}

那么main函数是一个栈帧,add是一个栈帧。
当程序运行时,main函数栈帧先被建立,这个栈帧在高地址。然后调用add函数。此时add函数栈帧被建立,在低地址。当程序执行流进入add函数时,add函数内的局部变量在add函数栈帧中被建立。然后add返回。当add函数返回,此时add函数栈帧被销毁,同时add函数内的局部变量也被销毁。所以,C/C++编程原则告诉我们:永远不要返回一个指向局部对象的指针。也就是说如下代码是错误的:

int* getNumber(void)
{
    int a = 3;
    return &a;
}

那么运行时的栈是什么样子的呢?它是一个随着运行,不断增长(进入新的函数调用)和缩短(函数返回)的动态影像。

从汇编语言看函数调用

以下是MASM文法编写的汇编语言程序

.386 ;386系统
.MODEL FLAT ;32位平坦地址模式

Exit PROTO NEAR32 stdcall, dwPara:DWORD ;退出函数原型
                                                ;Exit是函数名dwPara是函数参数

.STACK 4096 ;保留4096字节栈空间

.DATA ;数据段定义全局变量
number1 DWORD 11111111h ;定义变量number1大小4字节
number2 DWORD 22222222h ;定义变量number2, 大小4字节

.CODE ;程序代码
Init PROTO NEAR32 ;定义函数Init
        mov number1, 0 ;假设该指令地址为0x0040 0000

mov number2, 0
        ret ;函数Init返回
Init ENDP ;函数Init结束

_start: ;相当于main函数
        call Init ;调用函数Init此指令地址为0x0040 000f
         ...... ;该处指令地址为0x0040 0014
       
        INVOKE Exit, 0 ;调用Exit退出

PUBLIC _start ;公开入口点

END ;程序结束

其实代码不用看的...
假设程序被加载入内存,这时esp被初始化,然后esp指向栈顶。设此时栈顶地址为0x0063 00f8.一切为了说明方便哈。总之程序加载后,栈被初始化,也就是esp被初始化,esp会指向内存中的某个地址,并以这个地址作为栈的起始。
eip始终指向执行流,也就是“下一条指令”。

那么程序加载。栈初始化了。数据区域在内存中开辟出来了,全局变量被给予确切地址(这里是虚拟地址,因为这是一个进程,它的地址只管在虚拟地址空间中给就可以了,虚拟地址到物理地址的映射由操作系统和MMU完成)。代码段(也就是要执行的指令)也被放入内存中并给予确切地址。eip指向代码段的开始,并开始执行程序...

所以eip只管指向某个内存地址,这个内存地址存储着程序员编写的指令,然后CPU把指令取出来执行就是了。所以计算机叫做“顺序存储控制机”。对不起我啰嗦了。

好的。我们假设了,在程序加载后,esp被初始化为0x0063 00f8,并假设了mov number1, 0这个指令的地址在0x0040 0000,根据这个假设的地址和每个指令码的长度(这些指令都放在代码段,而且一个一个指令就是挨着放的),推断出call指令的地址是0x0040 000f,call指令的下一条指令的地址是0x0040 0014(因为这个call指令的长度占用5个字节,0x0040 000f + 5 = 0x0040 0014)。这里不算我对指令长度的计算错误,总之假设我的地址计算是正确的。

OK开始了。程序已经加载。那么开始程序执行。eip首先指向call指令,因为_start开始那里就是call指令。嗯,eip就是一个32位寄存器,这个寄存器里面的值永远是即将执行的指令的内存地址,这时eip里面的值是0x0040 000f。

call指令执行!该指令首先将下一条指令的地址压入栈,也就是说,call指令的第一个动作是将0x0040 0014(call指令的下一条指令地址)压入栈。esp此时变化,其值变为0x0063 00f4。为什么?因为esp被初始化为0x0063 00f8,一个地址4个字节入栈之后,esp = esp - 4。然后call指令转去调用Init过程代码。eip变化为0x0040 0000,为什么?因为Init过程的第一个指令地址就是0x0040 0000.这个过程是由CPU自动完成的,也就是说,call指令,让CPU自动完成这一系列动作。

然后Init过程执行到ret指令。
ret指令干什么?它将栈内数据弹出,并用该数据填充eip。栈内数据是什么?就是0x0040 0014,它就是call指令的下一条指令的地址!同时esp = esp + 4.也就是说,ret指令执行后,eip值变为0x0040 0014, esp的值变回0x0063 00f8.这个过程由CPU自动完成。ret指令让CPU自动完成这一系列动作。

整理:执行call,call指令首先将下一条指令地址入栈,然后跑去执行过程代码;过程代码中执行ret,ret首先从栈中将下一条指令地址弹回eip,这样程序就开始执行call指令后的指令。一句话:eip始终指向下一条指令地址。

以上!就是汇编函数调用和返回的过程。就是一个call和一个ret.eip在这个执行过程中通过栈来保存。

接下来,让我们开始考察C语言的过程调用和返回,也就是C语言函数的参数压栈和参数访问过程。

先看一个汇编调用压参和参数访问过程。

假设有一个add过程,这个过程的工作是将两个整型值(每个整型值4字节)相加,并将相加的和返回eax寄存器。
如果通过把参数压入堆栈来传递参数调用过程,那么调用方(caller)代码如下:

 push var1 ;第一个变量值
 push var2 ;第二个变量值
 call add ;调用add过程
 add esp, 8 ;从栈移除参数

而被调用过程(callee)add的代码如下:

add PROC NEAR32 ;add过程该过程将两个整型值相加
    push ebp ;保存基栈指针
    mov ebp, esp ;建立栈
    mov eax, [ebp + 8] ;复制第二个参数值(var2)
    mov eax, [ebp + 12] ;加上第一个参数值(var1)
    pop ebp ;恢复ebp寄存器
    ret ;过程返回
add ENDP ;过程结束

首先,esp是栈顶,直接从caller栈顶看起。也就是,在调用前,esp指向某个内存地址。
在调用函数前将参数压入栈中。
push var1
push var2
这两行代码使esp - 8. 然后压参完毕,图中即为压参完毕esp.
然后调用函数:
call add
嗯,之前复习call指令时说什么了?call指令执行时,首先将返回地址压入栈。
也就是将add esp, 8 这条指令的地址压入栈。

然后call指令执行过程调用,eip指向add函数内第一条指令的地址:
push ebp ;将ebp保存到栈中,同时esp - 4(说过了80X86的栈是向低地址方向增长的).
此时ebp原值被保存入栈中。
然后:
mov ebp, esp
此时以ebp为基准的栈建立了。此时ebp和esp都指向栈顶(ebp原值被栈保存起来了哦)。
为什么要这么做?
因为esp是随时变动的,只要有压栈和出栈的操作,esp的值就随着压栈和出栈的操作变化(随着push和pop操作变化,甚或,程序员直接改动esp的值)。
而ebp却不会随着push和pop操作变化。程序员在callee中不会修改ebp的值,而是使用ebp作为基准访问参数。

那么接下来就很好理解了,第二个参数的地址是ebp + 8, 第一个参数的地址是ebp + 12.
所以
mov eax, [ebp + 8] ;复制第二个参数值(var2)到eax
mov eax, [ebp + 12] ;加上第一个参数值(var1)
就不难理解了。

在过程把实现代码处理完毕的最后,pop ebp将ebp原值从栈中弹出恢复。
然后ret返回指令将返回地址弹出并赋给eip(请注意,返回地址弹出后,esp + 4, 这时esp正好指向调用者压参完毕的位置),...
回到调用者的地方并继续执行。

那么调用处的add esp, 8 ;从栈移除参数
是干什么用的?注释已经说得很清楚了。
调用者将var1和var2压到栈中,由于调用者的压栈,esp被往下移动了8;那么这个esp的原始位置也就是caller的栈顶应该在过程调用后恢复,add esp, 8就是恢复esp的。

ok。基本上就是如此了!

对于C语言的过程调用,比如,在main函数里面调用add

int main(int argc, char* argv[])
{
    ...
    add(x, y);
    ...
}

实际上,这里add(x, y)(调用者处)被编译器编译成如下汇编代码:

push y
push x
call add
add esp, 8

以上,这就是C过程调用的汇编解释。

接下来给出一般过程的入口代码和出口代码。

不难猜测,所有的过程(被调用函数)都有一样的入口代码和出口代码:

所有的C函数,在被编译器编译成汇编代码之后,
函数开始的几行汇编代码总是这样的,所以我们称这它为入口代码(entry code):

push ebp ;保存基址
mov ebp, esp ;建立ebp偏移基准
sub esp, n ;n个字节的局部变量参数
push ... ;保存过程中会用到的通用寄存器
...
pushf ;保存标识寄存器也就是保存标志位

而结尾的几行总是这样的,所以称其为出口代码:

popf ;恢复标识寄存器
pop ... ;恢复寄存器
...
mov esp, ebp ;恢复callee esp
pop ebp ;恢复ebp
ret ;返回

stdcall和cdcel


既然已经了解了上述内容,那么调用惯例就很容易理解了。
cdcel和stdcall是约定俗成的调用惯例,它们的区别在于由谁来恢复esp。

cdcel是由调用者恢复esp的调用惯例,
也就是说

push var1
push var2
call add
add esp, 8

这是cdcel调用惯例

而stdcall则是由callee恢复esp的调用惯例
stdcall会在callee里面将ret这样写:

ret 8

意思是返回的同时esp + 8.

这两种调用惯例,stdcall的好处是不用每次都在调用过程后写add esp, 8这样就减小了代码量,减小了目标文件的体积。
而stdcall的缺陷更明显,那就是callee有时候无法推断参数的个数和长度,这样的话esp只能由调用者恢复(比如变参数函数,这种函数callee是无法推断参数个数的,也就无法知道应该在ret后面加多少偏移量)。

EOF

Visual Studio下的dllimport和dllexport


title: Visual Studio下的dllimport和dllexport
date: 2014-11-24 17:39:00
tags:

  • C/C++
  • DLL

我相信写WIN32程序的人,做过DLL,都会很清楚__declspec(dllexport)的作用,它就是为了省掉在DEF文件中手工定义导出哪些函数的一个方法。当然,如果你的DLL里全是C++的类的话,你无法在DEF里指定导出的函数,只能用__declspec(dllexport)导出类。但是,MSDN文档里面,对于__declspec(dllimport)的说明让人感觉有点奇怪,先来看看MSDN里面是怎么说的:

不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量。

初看起来,这段话前面的意思是,不用它也可以正常使用DLL的导出库,但最后一句话又说,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量这个是什么意思?

那我就来试验一下,假定,你在DLL里只导出一个简单的类,注意,我假定你已经在项目属性中定义了 SIMPLEDLL_EXPORT:

SimpleDLLClass.h文件:

#ifdef SIMPLEDLL_EXPORT
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT
#endif

class DLL_EXPORT SimpleDLLClass
{
public:
  SimpleDLLClass();
  virtual ~SimpleDLLClass();

  virtual getValue() { return m_nValue;};
private:
  int m_nValue;
};

SimpleDLLClass.cpp文件:

#include "SimpleDLLClass.h"

SimpleDLLClass::SimpleDLLClass():m_nValue(0)
{
}

SimpleDLLClass::~SimpleDLLClass()
{
}

然后你再使用这个DLL类,在你的APP中include SimpleDLLClass.h时,你的APP的项目不用定义 SIMPLEDLL_EXPORT 所以,DLL_EXPORT 就不会存在了,这个时候,你在APP中,不会遇到问题。这正好对应MSDN上说的__declspec(dllimport)定义与否都可以正常使用。但我们也没有遇到变量不能正常使用呀。那好,我们改一下SimpleDLLClass,把它的m_nValue改成static,然后在cpp文件中加一行

int SimpleDLLClass::m_nValue=0;
如果你不知道为什么要加这一行,那就回去看看C++的基础。 改完之后,再去LINK一下,你的APP,看结果如何,结果是LINK告诉你找不到这个m_nValue。明明已经定义了,为什么又没有了??肯定是因为我把m_nValue定义为static的原因。但如果我一定要使用Singleton的Design Pattern的话,那这个类肯定是要有一个静态成员,每次LINK都没有,那不是完了? 如果你有Platform SDK,用里面的Depend程序看一下,DLL中又的确是有这个m_nValue导出的呀。
再回去看看我引用MSDN的那段话的最后一句。 那我们再改一下SimpleDLLClass.h,把那段改成下面的样子:

#ifdef SIMPLEDLL_EXPORT
#define DLL_EXPORT __declspec(dllexport)
#else
#define DLL_EXPORT __declspec(dllimport)
#endif

再LINK,一切正常。原来dllimport是为了更好的处理类中的静态成员变量的,如果没有静态成员变量,那么这个__declspec(dllimport)无所谓。

_declspec(dllexport)与_declspec(dllimport)都是DLL内的关键字,即导出与导入。他们是将DLL内部的类与函数以及数据导出与导入时使用的。主要区别在于,dllexport是在这些类、函数以 及数据的申明的时候使用。用过表明这些东西可以被外部函数使用,即(dllexport)是把DLL中的相关代码(类,函数,数据)暴露出来为其他应用程 序使用。使用了(dllexport)关键字,相当于声明了紧接在(dllexport)关键字后面的相关内容是可以为其他程序使用的。而 dllimport关键字是在外部程序需要使用DLL内相关内容时使用的关键字。当一个外部程序要使用DLL内部代码(类,函数,全局变量)时,只需要在 程序内部使用(dllimport)关键字声明需要使用的代码就可以了,即(dllimport)关键字是在外部程序需要使用DLL内部相关内容的时候才 使用。(dllimport)作用是把DLL中的相关代码插入到应用程序中。

   _declspec(dllexport)与_declspec(dllimport)是相互呼应,只有在DLL内部用dllexport作了声明,才能 在外部函数中用dllimport导入相关代码。实际上,在应用程序访问DLL时,实际上就是应用程序中的导入函数与DLL文件中的导出函数进行链接。而 且链接的方式有两种:隐式迎接和显式链接。

  隐式链接是指通过编译器提供给应用程序关于DLL的名称和DLL函数的链接地址,面在应用程序中不需要显式地将DLL加载到内存,即在应用程序中使用dllimport即表明使用隐式链接。不过不是所有的隐式链接都使用dllimport。

显式链接刚同应用程序用语句显式地加载DLL,编译器不需要知道任何关DLL的信息

以下是一个DLL头文件的正规编写方式:

#ifdef DIALOG_MAINMENU_EXPORTS
#define DIALOG_MAINMENU_API __declspec(dllexport) 
#else
#define DIALOG_MAINMENU_API __declspec(dllimport) 
#endif

class Dialog_MainMenu {
public:
    static DIALOG_MAINMENU_API enum GAME_STATES {
        MAINMENU, GAME, OPTIONS, CREDITS, QUIT
    };
    static DIALOG_MAINMENU_API GAME_STATES CurrentGameState;
    DIALOG_MAINMENU_API GAME_STATES GetState();
};

一下截取一段老外的解释:

OK - when you compile the dll - you are exporting the types. So, you need to define the static member in .cpp file of the dll. You also need to make sure that you have enabled the definition of DIALOG_MAINMENU_EXPORTS in compiler settings. This will make sure types are exported.

Now, when you link the console application with the dll - you will #include dll's header and dont enable any definition of DIALOG_MAINMENU_EXPORTS in compiler settings (just leave the settings default). This will make the compiler understand that now you are importing the types from your dll into exe application.

参考:

一次结对编程的亲身体验


title: 一次结对编程的亲身体验
date: 2017-10-14 10:30:46
tags:
- 敏捷开发
- 职业生涯

思绪来得也快去得也快。 -- 云风

前言

结对编程是敏捷软件开发中提出的一种实践和概念,意思就是两个程序员坐在一台电脑前一起编写代码,其中一个主要编写代码,另一个主要负责代码的review,提出自己的疑问和自己的见解。

一次亲身体验

在故事开始之前需要说点必要的题外话,项目组增加了人手,除了我以外增加了2个同事。客户端的GUI框架选择了Qt,新来的两个同事虽然已工作多年,比较有经验,但是他们两个目前暂时都对这个Qt框架没我熟悉和有经验。

昨天是周五,面临着项目又一次功能的迭代,一周的目标的收尾结束。所以我自己也比较忙,花了一早上把分配给自己的任务给搞定了。 由于我们项目的客户端新的选择的是Qt来开发的GUI界面,Qt带来一定的复杂性在对此不熟悉的同事直接上手介入项目的开发带来了一定困难,这个功能模块的任务原本是分配给一个刚学习Qt几天的同事来做的,但是由于项目进度的推进关系,必须又要在这周五有个收尾和结果,所以为了不影响进度又因为任务最开始是分配给他的,所以我提出了让那个同事一起过来跟我坐一个工位上一起实践结对编程。

就这样,主要由我来编写GUI界面和功能逻辑的代码,而他坐在旁边根据自己的思路提出疑问和见解,任务的目标功能很快就完成了,期间只用了2个多小时,比我预想得要快,而且代码的质量也比预想的要高,如果这个功能我自己一个人做的话,虽然也花不了多少时间,顶多3小时,但是这次的经历使我对结对编程产生了新的看法,两个人一起工作的效率竟然不低,反而可能经过多次这样的实践工作效率大大提高,代码质量也提高了。以前我对于结对编程的了解也仅仅限于各类软件开发的书本,比如《代码大全》。但这次却是让我真正感受到了效率。以下我就详细说说带来了哪方面的效率。

  • 有利于知识经验的分享和传递,特别适合于帮助主要负责Review的同事快速熟悉自己所不熟悉的领域(框架,库,业务逻辑等)。

  • 有效控制代码质量和风险,经过互相讨论的代码实现往往比自己独自决定的考虑更加全面。在一个人的注视下,反而不好意思写烂代码了,因为要边写代码边讲解自己的实现思路,同时负责Review的同事也会积极反馈你的代码逻辑,在这样的情况下,更容易编写可读性高的代码。

  • 在一个负责Review的同事的注视下的工作可以提高工作效率,分心的概率减小。因为一个人的注视下,专心编写代码的注意力会高度集中,Review代码逻辑的人也会由于积极的互相讨论注意力高度集中。

以上三点提到的效率,在完成任务目标后,都得到我们两个同事的一致肯定,互相都有收获。

结对编程不是银弹

因为结对编程只是敏捷开发中的一个方法论,所以它也不是能绝对带来效率上的提升的,没有银弹。它需要有一个前提,这个前提是在完成体验了一次结对编程之后,我总结出来的,这个前提也反应了结对编程可能出现的缺点。

  • 由于双方注意力的高度集中,几个小时实践下来,会很累,需要耗费很大的精力来实践。以至于很少有团队能长期这样坚持。

  • 双方的技术水平和编程品味差距不能太大,不然不利于交流。试想一个场景,Review代码的人看不惯编写代码的人的风格和实现算法,直接鄙视编码的同事,老子有更厉害的算法,老子更牛逼。 所以,这样是万万不行的。

  • 双方最好是熟悉项目不同模块的开发人员,比如我熟悉模块A,你熟悉模块B。这样一旦结对编程,互相收获会更大,经常这样,我也可以熟悉模块B,你也可以熟悉模块A了,有利于团队建设,分散节点风险,控制风险。比如,项目的某模块A使用了一个复杂性较高的算法,但是这个A模块是小明编写的,没其他人熟悉模块A,但是恰恰在项目的关键时期,小明请了婚假之类较长的假期,项目由于新功能的发布,需要结合业务修改模块A的算法,但是由于该算法有一定的复杂性,项目组内没有其他成员敢碰模块A,在有限的时间内,其他组员也可能完成模块A的修改,这样会导致项目只能拖延,公司可能流失客户。

  • 一些公司可能是看工作量,成果算KPI的。SVN,Git中都是编写代码的同事的commit更多,Review代码同事的commit更少。如果结对编程,那么Review代码的人岂不是活也没干? 怎么算员工绩效? 从一般的公司管理层的思维角度会认为,产出比太低,耗费公司资源。

归而结网,软件工程中提出的各种方法论目的无非就是尽量在更短的时间内快速交付并发布高质量的软件。这些各种方法论都不是万能的,都需要根据团队的特点适当选择,找出适合自己团队的最佳实践,应该靠不断分析、试错、调整,而不是照本宣科。

qsort的hack玩法


title: qsort的hack玩法
date: 2014-05-12 14:00:00
tags:

  • C/C++

无聊,写着耍耍。有本书叫短码之美,感受下短码的魅力。

#include <stdio.h>
#include <stdlib.h>

char cmp_shellcode[] = "\x55"
"\x89\xe5"
"\x8b\x4d\x08"
"\x8b\x45\x0c"
"\x8b\x10"
"\x8b\x01"
"\x29\xd0"
"\x5d"
"\xc3";

int main(n)
{
  int a = 2, b = 5;
  int data1[10] = {2,3,4,67,32,25,63,23,64, 88};
  
  int data2[10] = {43,15,42,13,44,24,54,33,1,10};

  qsort(data1, 10, sizeof(int),(int (*)(int *, int *))&cmp_shellcode);
  qsort(data2, 10, sizeof(int),"YXZQQQ\x8b\x00+\x02\xc3");

  for(n = 0; n < 10; printf("%d ", data1[n]), n++);
  
  puts("");
  
  for(n = 0; n < 10; printf("%d ", data2[n]), n++);

  return 0;
}

CMake简明教程


title: CMake简明教程
date: 2018-08-12 17:39:00
tags:

  • C/C++
  • CMake

前言

主要最近的换工作,完全在Linux下开发,虽然以前都接触过CMake,不过体系也是零散的,遂做了一个简短的CMake教程,以供后续快速入门。

另外,好久也没有写文章了,这份工作还是有一定的技术性,之前的那家公司是开发/维护,大部分工作都是维护,没有什么写文章的激情。

所以,今天是硬凑一篇文章。

正文

CMake

CMake是跨平台的元构建系统,也就是说,它不实际产生构建行为,它只是生成给其他构建系统使用的文件,比如Makefile,MSBuild的solution file。

CMake根据读取名为CMakeLists.txt的文件,然后生成平台特定的构建文件,但是一个很大的问题是,CMake官方提供的教程特别复杂,对于新手的个坑,很难快速入门。

这个教程会通过例子来学习怎么用CMake。以下我们提供几个C++源代码供例子使用:

  • main.cpp
  • vector.h
  • vector.cpp
  • array.h
  • array.cpp

那么描述构建的CMakeLists.txt内容会是以下:

cmake_minimum_required(VERSION 2.8)
project(pjc-lab5)

set(CMAKE_CXX_FLAGS "-std=c++14 -Wall ${CMAKE_CXX_FLAGS}")

include_directories(${CMAKE_CURRENT_SOURCE_DIR})

add_executable(vector-test
    array.cpp
    vector.cpp
    main.cpp
)

以上代码很简单,但是第一个问题是,它是不可移植的,因为它没有任何逻辑判断就设置了GCC/Clang的特定编译参数。

第二个问题是,它全局改变了include的搜索路径。

CMake也要有好的书写习惯,采用更加现代的方式来写CMake文件:

cmake_minimum_required(VERSION 3.5)
project(pjc-lab5 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)


add_executable(vector-test
    array.cpp
    vector.cpp
    main.cpp
)

注意了,以上代码有几点改变了:

  • 强制要求了CMake的版本不得小于3.5,因为要使用CMake的一些新功能
  • 直接指定该工程为C++工程。这样可以减少CMake查找tool chain的时间,它就不会去查找其他编译器了,也不会检查其他编译器是否正常了。
  • 直接采用可跨平台的方式来指定采用的C++标准为C++ 14
  • 打开CMAKE_CXX_STANDARD_REQUIRED开关,如果C++ 14标准不被支持,CMake会直接终止构建过程。反之,会采用老的标准来构建
  • CMAKE_CXX_EXTENSIONS开关是告诉CMake采用更加通用的编译参数,比如这个开关打开,传递给GCC的参数就会是-std=c++14 而不是-std=gnu++14

然后在构建过程中,你会发现没有警告,因为CMake不会设定编译器的警告级别,需要你根据不同平台来指定相应的编译器警告参数:

if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU" )
    target_compile_options( vector-test PRIVATE -Wall -Wextra -Wunreachable-code -Wpedantic)
endif()
if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang" )
    target_compile_options( vector-test PRIVATE -Wweak-vtables -Wexit-time-destructors -Wglobal-constructors -Wmissing-noreturn )
endif()
if ( CMAKE_CXX_COMPILER_ID MATCHES "MSVC" )
    target_compile_options( vector-test PRIVATE /W4 /w44265 /w44061 /w44062 )
endif()

如果就采用上述的CMake文件,那么它生成的工程文件并不好,没有预期,你会发现如果生成VS的solution,你打开工程,你会发现没有包含头文件(vector.h array.h)。因为CMake不理解C++语言,它只是构建工具。

所以CMake文件中要改变下:

add_executable(vector-test
    array.cpp
    vector.cpp
    main.cpp
    array.h
    vector.h
)

当然,也可以通过CMake的source_group命令给文件归类:

source_group("Tests" FILES main.cpp)
source_group("Implementation" FILES array.cpp vector.cpp)

这样VS工程下就可以看到对C++源文件分类的文件夹图标了。

Tests

CMake是一堆工具的集合,所以它有一个test runner,叫CTest

要使用它,你需要显式指定它:

add_test(NAME test-name COMMAND how-to-run-it)

测试返回0表示成功,返回其他值表示失败。

还可以自定义,通过set_tests_properties来设置其相关属性

对于我们的例子工程,我们仅仅是运行bin文件,并不做额外检查:

include(CTest)
add_test(NAME plain-run COMMAND $<TARGET_FILE:vector-test>)

COMMAND后面的表达式是generator-expression

最后,我们的CMakeLists.txt的内容会是:

cmake_minimum_required(VERSION 3.5)
project(pjc-lab5 
DESCRIPTION "Very nice project"
LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)


add_executable(vector-test
    array.cpp
    vector.cpp
    main.cpp
    array.h
    vector.h
)

source_group("Tests" FILES main.cpp)
source_group("Implementation" FILES array.cpp vector.cpp)


if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU" )
    target_compile_options( vector-test PRIVATE -Wall -Wextra -Wunreachable-code -Wpedantic)
endif()
if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang" )
    target_compile_options( vector-test PRIVATE -Wweak-vtables -Wexit-time-destructors -Wglobal-constructors -Wmissing-noreturn )
endif()
if ( CMAKE_CXX_COMPILER_ID MATCHES "MSVC" )
    target_compile_options( vector-test PRIVATE /W4 /w44265 /w44061 /w44062 )
endif()

include(CTest)
add_test(NAME plain-run COMMAND $<TARGET_FILE:vector-test>)

libraries

之前的教程都是很简单的例子,但是现实中的项目往往要拆分模块,链接外部的第三方库或者链接工程内的库。

如果不想阅读此章节,可以参考JetBrain的CLion提供的一个简明的CMake教程

里面记录如何包含链接外部的library。

构建生成一个library的cmake指令如下:

add_library(libname [STATIC | SHARED] two.cpp three.h)

下面我们来写一个计算器程序的cmake,bin文件依赖了lib文件,这样看起来就像一个小工程了:

cmake_minimum_required(VERSION 3.8)

project(Calculator LANGUAGES CXX)

add_library(calclib STATIC src/calclib.cpp include/calc/lib.hpp)
target_include_directories(calclib PUBLIC include)
target_compile_features(calclib PUBLIC cxx_std_11)

add_executable(calc apps/calc.cpp)
target_link_libraries(calc PUBLIC calclib)

变量和缓存变量

为什么要说这个?因为如果cmake维护一个很大的工程,会有各种编译策略参数,这个就需要逻辑判断,变量这些就随之诞生了。

给一个局部变量设置一个值:

set(MY_VARIABLE "value")

当然,cmake也有list类型的变量,有2种表达方式:

set(MY_LIST "one" "two")
set(MY_LIST "one;two")

以上变量如果离开作用域就无效了。

下面来说下缓存变量,如果你要从命令行来设置cmake的变量,那么就需要声明缓存变量。类似于CMAKE_BUILD_TYPE这样的变量都是缓存变量:

set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "Description")

但是以上不会替换已经存在的值,需要按照以下这样:

set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "" FORCE)
mark_as_advanced(MY_CACHE_VARIABLE)

环境变量

cmake可以访问环境变量:

set(ENV{variable_name} value)
set(MY_VAR $ENV{variable_name})

属性

cmake也会在属性中保存一些信息,这些属性有点像变量,但是这些属性的作用一般是针对一个target或者目录什么的。cmake的属性变量是以CMAKE_打头的,类似CMAKE_BUILD_TYPE,CMAKE_CXX_STANDARD这些。

比如使用CXX标准可以用设置属性的方式办到,有两种表达:

set_property(TARGET TargetName
             PROPERTY CXX_STANDARD 11)

set_target_properties(TargetName PROPERTIES
                      CXX_STANDARD 11)

当然既然有set,当然有get:

get_property(ResultVariable TARGET TargetName PROPERTY CXX_STANDARD)

Cmake编程

控制流

if(variable)
    # If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number
else()
    # If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND`
endif()
if("${variable}")
    # True if variable is not false-like
else()
    # Note that undefined variables would be `""` thus false
endif()

宏和函数

function(SIMPLE REQUIRED_ARG)
    message(STATUS "Simple arguments: ${REQUIRED_ARG}, followed by ${ARGV}")
    set(${REQUIRED_ARG} "From SIMPLE" PARENT_SCOPE)
endfunction()

simple(This)
message("Output: ${This}")

与源码文件“通信”

Cmake允许源代码访问cmake的变量,这个指令就是configure_file。

这样的功能在版本管理上经常使用:

Version.h.in

#pragma once

#define MY_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MY_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MY_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MY_VERSION_TWEAK @PROJECT_VERSION_TWEAK@
#define MY_VERSION "@PROJECT_VERSION@"
configure_file (
    "${PROJECT_SOURCE_DIR}/include/My/Version.h.in"
    "${PROJECT_BINARY_DIR}/include/My/Version.h"
)

可以从上面的配置看出来,本质上cmake只是把后缀in的文件,进行变量标记替换,然后再拷贝到指定目录的文件名。没有什么神奇的。

当然,cmake也可以反过来,cmake代码访问源码文件的内容:

# Assuming the canonical version is listed in a single line
# This would be in several parts if picking up from MAJOR, MINOR, etc.
set(VERSION_REGEX "#define MY_VERSION[ \t]+\"(.+)\"")

# Read in the line containing the version
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/include/My/Version.hpp"
    VERSION_STRING REGEX ${VERSION_REGEX})

# Pick out just the version
string(REGEX REPLACE ${VERSION_REGEX} "\\1" VERSION_STRING "${VERSION_STRING}")

# Automatically getting PROJECT_VERSION_MAJOR, My_VERSION_MAJOR, etc.
project(My LANGUAGES CXX VERSION ${VERSION_STRING})

怎样规划你的工程结构

基本如下:

- project
  - .gitignore
  - README.md
  - LICENCE.md
  - CMakeLists.txt
  - cmake
    - FindSomeLib.cmake
  - include
    - project
      - lib.hpp
  - src
    - CMakeLists.txt
    - lib.cpp
  - apps
    - CMakeLists.txt
    - app.cpp
  - tests
    - testlib.cpp
  - docs
    - Doxyfile.in
  - extern
    - googletest
  - scripts
    - helper.py

运行其他的程序

编译的时候要做很多事情,比如你构建完毕要用nsis进行打包等等,有些时候不得不调用其他命令行。

配置时运行一个命令

find_package(Git QUIET)

if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
    execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
                    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                    RESULT_VARIABLE GIT_SUBMOD_RESULT)
    if(NOT GIT_SUBMOD_RESULT EQUAL "0")
        message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
    endif()
endif()

编译时运行一个命令

find_package(PythonInterp REQUIRED)
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp"
    COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/GenerateHeader.py" --argument
    DEPENDS some_target)

add_custom_target(generate_header ALL
    DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp")

install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp DESTINATION include)

Cmake其他的一些特性

CMAKE_BUILD_TYPE变量默认既不是Debug也不是Release。必须指定。

关于C++ 11等一些新标准,有以下配置:

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

set_target_properties(myTarget PROPERTIES
    CXX_STANDARD 11
    CXX_STANDARD_REQUIRED YES
    CXX_EXTENSIONS NO
)

查找库

find_library(MATH_LIBRARY m)
if(MATH_LIBRARY)
    target_link_libraries(MyTarget PUBLIC ${MATH_LIBRARY})
endif()

开始学编译原理一点有趣的东西


title: 开始学编译原理一点有趣的东西
date: 2017-03-28 10:48:27
tags:
- 编译原理
- 程序语言理论

唉,好像又回到大学被《编译原理》狂虐的时代了。因为最近在设计自己的脚本语言解释器,由于词法分析阶段用正则文法(3型文法)就能搞定,但是语法分析阶段不学习上下文无关文法(2型文法)是不行的了,所以出来混迟早是要还的

我在之前的文章《什么是类型安全》提到过著名语言学家乔姆斯基的一个例子,是为了说明语法和语义的区别。其实乔姆斯基作为世界顶级的语言学专家为语言学的研究发展做出了很大的贡献,不要以为人类的语言与程序设计语言没有半点关系(确实,它们之间的关系并不大),乔姆斯基的贡献之一,就是在形式语言(formal language)领域提出了著名的乔姆斯基层级谱系(Chomsky hierarchy),它把所有的形式语言的形式文法分成了四大类,分别是0型文法,1型文法,2型文法,3型文法。从包含关系上来说,0型文法 > 1型文法 > 2型文法 > 3型文法。 符号'>'表示,X范围大于X, 0型文法是1,2,3型文法的超集(superset)。

那么计算机编程语言到底与上面的形式文法有什么关系呢?因为计算机编程语言的定义就需要用到上面的形式化文法来描述。

其实很多计算机相关的语言背后都有一定的基础。比如:

  • LISP,背后是λ演算,这个数学基础给了LISP非常强大的表达能力;(虽然多数人不直接用LISP)至少,LISP给现在各种支持函数式编程的语言提供了借鉴。

  • 正则表达式。背后是正则文法(也就是3型文法)。凡是可以使用正则文法定义的语言,都可以使用正则表达式定义描述。当然,经常有人试图用它来匹配各种编程语言的代码,这基本上是肯定要出bug的。原因很简单,多数主流编程语言都是『上下文无关语言』,它是正则语言的超集(参考乔姆斯基层级),记住,正则语言一定是上下文无关的,但是上下文无关的语言不一定能用正则语言来描述定义。

  • BNF范式。背后是上下文无关文法(也就是2型文法)。这也是为什么各种编程语言(即使复杂如C++或C#,还包括SQL和正则表达式)的规范文档,甚至不少『标准格式』(如JSON,URI等)的规范文档都喜欢用BNF或EBNF定义。更好玩的是,当你用BNF定义好一门语言时,还可以使用一种称为Parser Generator的程序(如YACC及各语言上的移植,Flex,Bison,ANTLR等,它们有时候也被称作编译器的编译器)来生成这门语言的解析程序(Parser)!当然了,一般工业制作的编程语言的编译器前端部分大部分会这么干,定义好语言描述,用工具直接就生成该语言的抽象语法树了(abstract snytax tree), 不需要自己写词法分析和语法分析了,避免不必要的苦力劳动。为什么能做到这么厉害的功能?这涉及到计算理论的很多知识,但归根到底,就是上下文无关文法。

参考

单例模式的线程安全


title: 单例模式的线程安全
date: 2017-02-27 15:48:40
tags:

  • 设计模式
  • 线程安全

在写单例模式的时候,一般我们都需要保证这个单例类的线程安全,当然,网络上有大部分“解决方案了”,加锁和双重检查锁配合来“保证”单例类的线程安全,可是,如果把指令重排序也考虑到其中的话,这样的写法,就是非线程安全了。

随便列出网络上几篇博文的单例模式都不是线程安全的:

以下是网络上大部分的“经典”的“线程安全”单例模式实现:

@ actually not thread safe
class singleton
{

private:
    static singleton* p{nullptr};
public:
    static std::mutex m_mutex;
    static singleton* getInstance();
public:
    singleton(const singleton& s) = delete;
};

singleton* singleton:: getInstance()
{
    // double-check locker
    if (p == nullptr) // first check
    {
        
        {
            std::lock_guard<std::mutex> lock(m_mutex); // lock
            if (p == nullptr)   // second check
                p = new singleton(); // but this may cause non-thread-safe
        }
        
    }
    return p;
}

首先,先讲解一下,指令重排会发生在系统的好几个层面,我分别用C++和Java来讲解:

  • C++ 被编译器编译成机器码的时候,机器码的顺序可能被重排过,在程序运行的时候,在CPU内部也可能会选择性的又进行一次重排,总共2次,重排是必定会发生的。

  • Java 被编译器编译成字节码的时候会重排一次,JVM执行字节码的时候又进行一次重排,JVM执行的字节码最终也是变成机器码在CPU内部又会进行重排,总共是3次。

所以了,到头来,指令重排是不可能消除的,这是编译器和CPU优化的领域,就不进行过多的探讨。

下面来讲解一下以上的单例类的代码,以及为什么它在指令重排序的情况下会变成非线程安全:

我上面把代码的注释写了,问题就出现在new 那里,熟悉C++的开发者可能知道new语句的大概执行动作,我把它分解成如下几步:

  1. 分配对象的内存空间,可以简单理解为malloc
  2. 调用对象的构造函数初始化对象(内存空间上的对象状态此刻是合法的了)
  3. 把内存空间的地址赋值给p指针

注意了,如果编译器生成的代码,和CPU内部的执行顺序永远是按以上的顺序执行,那么永远都不用担心线程安全的问题,但是由于happens-before语义,2和3步骤没有严格的依赖顺序,编译器有些时候为了优化,完全可以把3放到2时候执行,2也会放到3时候执行。那么这样情况下,可以就会变成下面这样了:

  1. 分配对象的内存空间,可以简单理解为malloc
  2. 把内存空间的地址赋值给p指针 (此刻内存空间上的对象状态不合法,没初始化)
  3. 调用对象的构造函数初始化对象(内存空间上的对象状态此刻是合法的了)

还想深入了解happens-before的,可以看这里

那么,指令重排下,以上的单例代码可能变成以下:

@ actually not thread safe
class singleton
{

private:
    static singleton* p{nullptr};
public:
    static std::mutex m_mutex;
    static singleton* getInstance();
public:
    singleton(const singleton& s) = delete;
};

singleton* singleton:: getInstance()
{
    // double-check locker
    if (p == nullptr) // first check
    {
        
        {
            std::lock_guard<std::mutex> lock(m_mutex); // lock
            if (p == nullptr)   // second check
             {
                    //以下是抽象出来的伪代码
                    memory_addr = malloc();   //1:分配对象的内存空间  
                    p = memory_addr;     //2:设置p指向刚分配的内存地址  
                                 //注意,此时对象还没有被初始化!  
                    singleton_constructor(memory);  //3:初始化对象  
             }
        }
        
    }
    return p;
}

聪明的人,一下子就可以看出来以上的代码在多线程下可能会导致的问题了,
还不明白的话,我做了一个线程执行时间表:

时间序列 线程A 线程B
t1 分配对象的内存空间
t2 设置p指向内存空间
t3 判断p是否为nullptr
t4 由于p不为nullptr,线程B将访问p指向的对象(而这个时候对象还没有初始化)
t5 初始化对象(调用构造函数)
t6 访问p指向的对象

线程B拿到一个未初始化的对象(对象状态不合法)去操作,结果肯定就出错了。

那么如何写才能保证线程安全呢? 我给出几种方案:

@ thread safe
// 以下代码虽然是C++ 11的,但是你可以把它理解非C++ 11,如果是C++ 11的话,从标准上就保证 静态初始化就是线程安全的,完全没必要像下面这样做了
class singleton
{

private:
    static atomic < singleton* > p{nullptr};
public:
    static std::mutex m_mutex;
    static singleton* getInstance();
public:
    singleton(const singleton& s) = delete;
};

singleton* singleton:: getInstance()
{
    // double-check locker
    if (p == nullptr) // first check
    {
        
        {
            std::lock_guard<std::mutex> lock(m_mutex); // lock
            if (p == nullptr)   // second check
             {
                    p = new singleton(); 
             }
        }
        
    }
    return p;
}

如果是C++ 11的代码,完全没必要这么繁琐了,可以直接像下面这么干:

// C++ 11标准保证这是线程安全的,当然也得看编译器产商怎么实现了,比如悲剧的是
// VS2013下这么做是非线程安全的,但VS2015下绝对是线程安全
// 参考: https://msdn.microsoft.com/en-gb/library/hh567368.aspx
singleton*& getInst()
{
    static singleton* p = new singleton();
    return p;
}

详情请戳这里

如果是Java代码,建议按以下这么干,保证线程安全:

public class singleton {
    private static class Holder {
        static final singleton INSTANCE = new singleton();
    }

    public static singleton getInstance() {
        return Holder.INSTANCE;
    }
    // rest of class omitted
}

以上Java代码完全不需要加锁,Java的类加载器已经保证必定会在访问类的时候,最先初始化singleton。

详情请戳这里这里

EOF

互联网显然颠覆不了传统行业


title: 互联网显然颠覆不了传统行业
date: 2016-02-28 08:52:10
tags:
- 互联网

知乎上由于程序员居多,互联网火热,被一些小白搞得乌烟瘴气,个别还高喊着互联网颠覆传统行业,把程序员这职业吹上了天,听起来像是那么一回事,不过估计都初入行业的菜鸟,跟国内小编吹牛一个样

尽管现在编程越来越日常化,各行各业的技术人员都可能接触编程,但是这并不代表互联网介入并颠覆了它。在传统工业中,显然程序员是种被边缘化的职业,虽然很多重要技术职业需要编写代码,比如自动化控制出身的,需要编写各种控制算法,PID算法等,但是这些人可不自称程序员。计算机科学与自动控制是两码事,两种相差很大的领域,自动化毕业的优秀学生,能进宝马,轮船,电网负载等行业部门,也就是说自动化工程师比程序员分布在更广泛的领域,天上飞的,水里游的,马路上跑的,工厂里动的,你身处周围的绝大部分现代化设备里面都需要算法,包括特斯拉和火箭回收,一个简单的事实,用到这些算法都不是计算机专业学的,计算机专业也不开相关课程,专业都不在一个院,生产他们和写这些主要算法的人都叫xx工程师,但我们一般不叫他们程序员。例如,机械工程师会运用lisp语言作为cad的dsl辅助,但没人管他们叫程序员,他们都不是会像cs专业的程序员一样写代码,在任何国家行业分类上他们是100%的传统行业。如果以后不深入一个领域的知识技能,是不能立足的,互联网这种,都不叫啥领域,反而太泛泛了。一个程序员,会写几段代码,写了几段轮船,电网负载预测,航空遥感,车载系统的代码就敢说这个领域的?你难道没感觉出来你的代码实现和业务需求都是该领域的专家跟你描述指导的么?是不是互联网现在太火了,工资虚高,导致一些互联网程序员有些目中无人的样子,呵呵,去个电网公司写点代码,就敢说颠覆该领域,你开玩笑吧?请问你取代颠覆哪个部门了,你抢了那些领域人才的饭碗了?你真懂这些领域?电力专业的课程有这些,基础课不谈,电磁场与电磁波学三个学期,automatic control两个学期,power supply systems 1, optimization and operation of electricity and gas, components and devices of electicity supply, electrical machine1-2, power electricity 1-2……

请问你的专业知识能取代颠覆谁啊,你在该领域价值多少?核心相关电控程序时你写的?搞清楚自己位置吧,合作分工才是本质。还有,你不了解那些知识,在相关专业还只是能称作传统行业cs部门的码农。等你真了解了那些知识,也在离cs的道路上越走越远了。

最后,请问除了没技术门槛的服务业,互联网究竟颠覆哪个传统工业了?你看,不只有滴滴打车,饿了么,这些送饭,拉车,大型餐馆点餐的服务软件才能有颠覆的趋势么?

互联网从来没有打算颠覆传统行业,只是从旁改善传统行业的业务效率,现如今都在弄企业信息化也是这么个道理,物联网的概念也是慢慢这么出来的。

程序的含义


title: 程序的含义
date: 2017-10-15 10:30:46
tags:
- 语义学
- 程序语言理论

武士的刀,不应以刀鞘束缚,而应该以你的灵魂来约束。这个时代已经不需要武士了,但无论时代如何变迁,人都会有不能忘却的东西。即使有一天弃剑的时代到来,这一灵魂约束的正直之剑也绝不能丢弃。 -- 志村 《银魂》

前言

编程语言,以及我们用编程语言所写的程序,这些都是软件工程师工作的基础。我们用编程语言和程序阐明复杂的想法,并在彼此间交流这些想法,当然最重要的是在计算机中实现这些想法。

程序员是注重实践实际的职业,他们通过阅读文档,学习教程,研究现有的程序以及修改自己的简单程序来学习新的编程语言,而不会过多的思考那些程序有什么含义。然而,计算机编程不单是与程序相关,重要的是程序员要表达**。程序只是**的静态表示,是曾经存在于程序员脑海中的某个结构的快照。程序是有了含义才值得写下来的。那么是什么把代码和它的含义连接在一起的呢?怎样才能能将一个程序的含义描述得更具体一点呢?

“含义”的含义

语言学中,语义学(semantics)研究的是单词和它们含义之间的关系。比如,dog只是纸上一些符号的组合,或者由某人的声带引起的一系列空气震动,这与真正的狗或者通常意义上的狗的概念极为不同。语义不止关注抽象含义本身的基本性质,还关注具体的记号如何与它们抽象含义关联起来。

在计算机科学里,形式语义学偏向的是找到确定程序难以捉摸的含义的方法,并利用这些方法发现或者证明编程语言中有趣的东西。从定义新的编程语言和编译优化这种具体应用,到构造程序正确性的数学证明这样更抽象的领域,林林种种。这是一般的软件工程师接触不到的概念。

为了完整的定义编程语言,第一需要语法,描述程序看起来是什么样子;第二,语义,描述程序的含义。

但是许多语言都没有官方的书面规范,而只有一个可用的解释器或编译器。比如Ruby语言,它就没有标准规范,而是靠Matz先生的Ruby解释器(MRI)来规范的,也就是靠实现规范,任何一份Ruby的文档如果与MRI实际行为不一致,那么一定是文档错了。类似的语言还说有PHP和Perl。

另一些语言,是像C++,ECMAScript,Java描述编程语言的方法就是写一份平实的官方文档(一般是英文)。这些语言的标准化通过专家组成的委员会写成,与语言的实现无关,以至于这些语言的编译器解释器实现方式多种多样。通过官方规范定义一种语言更为严谨,这样所做的设计决策更有可能是经过深思熟虑的,而不是某一个特定实现的意外结果。但是语言标准规范,为了严谨化,通常非常难懂,文档中有没有矛盾,歧义的地方很难考证,这样的一份英文文档没有形式化的方法可以验证推导。

还有一种第三种方法来定义描述编程语言,就是使用形式语义学中的数学方法准确的描述编程语言的含义。这样的做法不仅能用适合系统分析甚至自动化分析的格式写出规范,还能保证其完全没有歧义,这样就可以对规范是否一致,是否含有冲突,以及是否有疏漏进行全面检查。

语法

传统的计算机程序是长长的字符串。每一种编程语言都有一系列的规则,描述那种语言中什么样的字符串被认为是有效程序。这些规则定义了这种语言的语法。

一个语法正确的程序能够被编译器正确的通过编译,也就是语法解析器能正确的处理这些字符串到一个抽象语法树(AST),那么这些字符串就是语法正确的。

然而,语法关心的是程序的表面是什么样子,而不是它的含义。程序有可能语法正确但是没有任何实际意义。比如 z = "hello" + 1可能会在运行时甚至是编译器报错,因为它试图在一个字符串型的值上加数值型数据。这些含义就是解释器,编译器来决定的。取决于解释器怎么去解释它。

正如我们所料,能说明如何把一种编程语言的语法与这个语法所暗含的语义对应起来的唯一方法并不存在。实际上,关于程序的含义有几种不同的研究方法,它们都在形式化,抽象,可表达性和实际效率之间做了权衡。

操作语义

考虑程序含义的最实际方法是思考它做了什么:在运行程序的时候,我们期望发生什么,在编程语言运行时中的不同结构是如何表现的,把它们放到一起组成更大的程序时会是什么效果。

这是操作语义学的基础,这种方法为程序在某种机器上的执行定义一些规则,依次捕捉编程语言的含义。这个机器一般是一种抽象机器(可以简单理解为某种特殊的解释器)。

有了操作语义,就可以朝着严谨而准确地研究语言中特定结构的目标前进了。用英语写成的语言规范可能暗藏着二义性,但一个形式化的操作性规范不会如此。

小步语义

那么,如何设计一台抽象机器并使用它定义一种编程语言的操作语义呢?一种方法就是假想一台机器,用这台机器直接按照这种语言的语法进行操作一小步一小步地对其进行反复规约(Reduce),从而对一个程序求值,无论最终结果如何,每一步都能让程序接近最终结果。

这种小步规约,类似数学中的代数求值:

(1 * 2) + (3 * 4)
=> 2 + (3 * 4)
=> 2 + 12
=> 14

我们可以认为14就是最终结果,因为根据定义,14这样的具体值有自己的含义已经不能再进一步规约了。

把如何进行每一小步的规约写成形式化规则,这个非形式化的过程就可以转换成一个操作语义。这些规则本身需要用某种语言(元语言)写下来,一般来说,这样的语言是数学符号。

比如,以下就是某种语言(暂且成为Simple语言)采用小步语义的数学化描述:

(这里要加一个图片,小步语义的)

从逻辑上将,这是一个推导规则的集合,它定义了基于这个语言抽象语法树的一个规约关系。实际点讲,这是一堆怪异的符号,很难让人理解这些是什么东西。

现在我们不会试图直接理解讲解这种形式化的符号,而是研究如何利用程序设计语言编写同样的推导规则。当然据王垠所说,大部分的程序语言理论的论文里面都有这样大量的数学符号,让人眼花缭乱的逻辑公式,符号。如果你看透这些符号的本质,就会发现这些深奥的符号其实相当于解释器的代码,只不过是用一种晦涩难懂的形式化逻辑语言写出来的。所以呢,为了讲清楚这些符号背后的本质,我们直接通过可读性高的程序设计语言来直接构造推导关系。

如果有代码基础的人,这里可以看到我写的完整小步语义规约Simple语言的代码,是采用Ruby来构造的。

  1. 表达式

首先来研究下Simple语言中表达式的语义。规则将作用于这些表达式的抽象语法树,所以我们必须把Simple表达式表示成Ruby对象。要做到这一点,一种方式就是为了Simple语法中的每一种不同的元素定义一个Ruby类,包括数字,加法,乘法等。然后把每一个表达式表示成这些类的实例构造成一颗树。

下面是数字的加减乘除的Ruby代码:

class Number < Struct.new(:value)
 def to_s
   value.to_s
 end

 def inspect
   "<<#{self}>>"
 end
end

class Add < Struct.new(:left,:right)
  def to_s
    "(#{left} + #{right})"
  end

  def inspect
    "<<#{self}>>"
  end
end

class Minus < Struct.new(:left,:right)
  def to_s
    "(#{left} - #{right})"
  end

  def inspect
    "<<#{self}>>"
  end
end

class Multiply < Struct.new(:left,:right)
  def to_s
    "(#{left} * #{right})"
  end

  def inspect
    "<<#{self}>>"
  end
end

class Divide < Struct.new(:left,:right)
  def to_s
    "(#{left} / #{right})"
  end

  def inspect
    "<<#{self}>>"
  end
end

为了测试以上代码的功能,手工实例化这些类来构造抽象语法树:

# 构造表达式树
# => 1*2 + 3*4
Add.new(                                          
    Multiply.new(Number.new(1), Number.new(2)),   
    Multiply.new(Number.new(3),Number.new(4))
)

以上代码只会在控制台上打印表达式,但是不会对表达式进行计算,因为类中没有实现对应类的规约推导规则。

现在要为抽象语法树定义规约方法,这将是实现一个小步操作语义的起点。也就是说,代码可以以一个抽象语法树作为输入,然后生成一个规约树作为输出。

在实现规约之前,首先要区分什么是能规约的,什么是不能规约的。显然对于加减乘除这样的表达式总是能规约的,但是Number表达式总是代表一个具体数值,显然不能规约了。

下面就用类似组合子编程的**来为每个表达式类编写规约的方法,每一个类判断自身是否能规约,并对自身类型作出规约的步骤,可以简单看成一种类型对应一个小型的解释器,它们只对自身的类型解释负责:

class Number < Struct.new(:value)
 def to_s
   value.to_s
 end

 def reducible?
   false
 end

 def inspect
   "<<#{self}>>"
 end
end

class Add < Struct.new(:left,:right)
  def to_s
    "(#{left} + #{right})"
  end

  def reducible?
   true
  end

  def reduce
    if left.reducible?
      Add.new(left.reduce,right)
    elsif right.reducible?
      Add.new(left,right.reduce)
    else
      Number.new(left.value + right.value)
    end
  end

  def inspect
    "<<#{self}>>"
  end
end

class Minus < Struct.new(:left,:right)
  def to_s
    "(#{left} - #{right})"
  end

  def reducible?
   true
  end

  def reduce
    if left.reducible?
      Minus.new(left.reduce,right)
    elsif right.reducible?
      Minus.new(left,right.reduce)
    else
      Number.new(left.value - right.value)
    end
  end

  def inspect
    "<<#{self}>>"
  end
end

class Multiply < Struct.new(:left,:right)
  def to_s
    "(#{left} * #{right})"
  end

  def reducible?
   true
  end

  def reduce
    if left.reducible?
      Multiply.new(left.reduce,right)
    elsif right.reducible?
      Multiply.new(left,right.reduce)
    else
      Number.new(left.value * right.value)
    end
  end

  def inspect
    "<<#{self}>>"
  end
end

class Divide < Struct.new(:left,:right)
  def to_s
    "(#{left} / #{right})"
  end

  def reducible?
   true
  end

  def reduce
    if left.reducible?
      Divide.new(left.reduce,right)
    elsif right.reducible?
      Divide.new(left,right.reduce)
    else
      Number.new(left.value / right.value)
    end
  end

  def inspect
    "<<#{self}>>"
  end
end

reduce方法总是构建出一个新的表达式,而不是对已有的进行修改。可以对其进行反复调用,从而通过很多步骤求出表达式的结果。

# => false
Number.new(4).reducible?
# => true
Add.new(Number.new(3),Number.new(7)).reducible?

# => 1*2 + 3*4
expression = Add.new(                                          
    Multiply.new(Number.new(1), Number.new(2)),   
    Multiply.new(Number.new(3),Number.new(4))
)

#直到规约终止,表达式的值也计算完成
# => true
expression.reducible?

# => 2 + 3*4
expression = expression.reduce

# => true
expression.reducible?

# => 2 + 12
expression = expression.reduce

# => true
expression.reducible?

# => 14
expression = expression.reduce

# => false
expression.reducible?

以上都是手工一步一步调用规约的,为了节省力气,可以用代码来模拟手工的规约操作,这是一个抽象机器,其实就是一个解释器,这个解释器自动对表达式进行规约求值。这是必然的,不然人类为啥要创造编译器,解释器这种东西呢?最简单的解释器无非就是一个函数或类,它接收一个表达式(输入),然后规约到最终结果(输出)。

class AbstractMachine < Struct.new(:statement)
  def step_next
    self.statement = statement.reduce
  end

  def run
    while statement.reducible?
      puts "#{statement},#{var_enviroment}"
      step_next
    end
    puts "#{statement}"  # final state
  end
end

实例化一个虚拟机让它运行:

=begin
  3*2 +  (10 - 8/4)
  6   +  (10 - 8/4)
  6   +  (10 - 2)
  6   +  8
  14
=end
AbstractMachine.new(
   Add.new(                                          
    Multiply.new(Number.new(3), Number.new(2)),   
    Minus.new(
        Number.new(10),
        Divide.new(Number.new(8),Number.new(4)))
   )
).run

这个Simple语言的表达式解释到这里就完成了,但是这样的语言不是图灵完备的,因为程序还不支持控制流,就是有while,if,bool类型(与或非),大于小于等比较。要扩展很容易:

class Boolean < Struct.new(:value)
  def to_s
    value.to_s
  end

  def inspect
    "<<#{self}>>"
  end

  def reducible?
    false
  end
end

class LessThan < Struct.new(:left, :right)
  def to_s
    "#{left} < #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce
    if left.reducible?
      LessThan.new(left.reduce,right)
    elsif right.reducible?
      LessThan.new(left,right.reduce)
    else
      Boolean.new(left.value < right.value)
    end
  end
end

class GreaterThan < Struct.new(:left, :right)
  def to_s
    "#{left} > #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce
    if left.reducible?
      GreaterThan.new(left.reduce,right)
    elsif right.reducible?
      GreaterThan.new(left,right.reduce)
    else
      Boolean.new(left.value > right.value)
    end
  end
end

class Equal < Struct.new(:left, :right)
  def to_s
    "#{left} == #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce
    if left.reducible?
      Equal.new(left.reduce,right)
    elsif right.reducible?
      Equal.new(left,right.reduce)
    else
      Boolean.new(left.value == right.value)
    end
  end
end

class LessEqualThan < Struct.new(:left, :right)
  def to_s
    "#{left} <= #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce
    if left.reducible?
      LessEqualThan.new(left.reduce,right)
    elsif right.reducible?
      LessEqualThan.new(left,right.reduce)
    else
      Boolean.new(left.value <= right.value)
    end
  end
end

class GreaterEqualThan < Struct.new(:left, :right)
  def to_s
    "#{left} >= #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce
    if left.reducible?
      GreaterEqualThan.new(left.reduce,right)
    elsif right.reducible?
      GreaterEqualThan.new(left,right.reduce)
    else
      Boolean.new(left.value >= right.value)
    end
  end
end

再次用虚拟机测试以上的代码:

=begin
  6 < (2 + 12)
  6 < 14
  true
=end
AbstractMachine.new(
   LessThan.new(
     Number.new(6),
     Add.new(
       Number.new(2),
       Number.new(12)))
).run

=begin
  6 == (2 + 4)
  6 == 6
  true
=end
AbstractMachine.new(
   Equal.new(
     Number.new(6),
     Add.new(
       Number.new(2),
       Number.new(4)))
).run

=begin
  6 <= (2 + 4)
  6 <= 6
  true
=end
AbstractMachine.new(
   LessEqualThan.new(
     Number.new(6),
     Add.new(
       Number.new(2),
       Number.new(4)))
).run

以上代码按照意图正确规约到最终结果了。

是不是觉得完了? 等等,一个正常的编程语言往往不会缺失变量这个语言特性吧?对,到现在,这门Simple语言还不支持变量!在任何有用的语言中,我们都期望在讨论值时能够使用有意义的名字,而不是它们本身的字面值(literal value)。所以我们引入一个新的表达式类Variable来支持变量:

class Variable < Struct.new(:name)
  def to_s
    name.to_s
  end

  def inspect
    "<<#{self}>>"
  end

  def reducible?
    true            
  end

  def reduce(var_enviroment)
    var_enviroment[name]     
  end
end

变量是可以规约的,它只是值的名字,本质上变量与值是一个键值对的关系。所以它的规约很简单,就是在变量上下文中根据名字查找其对应的值,所以从最简单的构造,可以把这个环境上下文设计为一个Hash表的数据结构,Ruby有内建的表示了,所以很简单。

所以为了支持变量定义,加减乘除等表达式类的规约方法必须得接收一个变量上下文环境的参数:

class Add < Struct.new(:left,:right)
  def to_s
    "(#{left} + #{right})"
  end

  def reducible?
   true
  end

  def reduce(var_enviroment)
    if left.reducible?
      Add.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      Add.new(left,right.reduce(var_enviroment))
    else
      Number.new(left.value + right.value)
    end
  end

  def inspect
    "<<#{self}>>"
  end
end

class Minus < Struct.new(:left,:right)
  def to_s
    "(#{left} - #{right})"
  end

  def reducible?
   true
  end

  def reduce(var_enviroment)
    if left.reducible?
      Minus.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      Minus.new(left,right.reduce(var_enviroment))
    else
      Number.new(left.value - right.value)
    end
  end

  def inspect
    "<<#{self}>>"
  end
end

class Multiply < Struct.new(:left,:right)
  def to_s
    "(#{left} * #{right})"
  end

  def reducible?
   true
  end

  def reduce(var_enviroment)
    if left.reducible?
      Multiply.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      Multiply.new(left,right.reduce(var_enviroment))
    else
      Number.new(left.value * right.value)
    end
  end

  def inspect
    "<<#{self}>>"
  end
end

class Divide < Struct.new(:left,:right)
  def to_s
    "(#{left} / #{right})"
  end

  def reducible?
   true
  end

  def reduce(var_enviroment)
    if left.reducible?
      Divide.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      Divide.new(left,right.reduce(var_enviroment))
    else
      Number.new(left.value / right.value)
    end
  end

  def inspect
    "<<#{self}>>"
  end
end

class LessThan < Struct.new(:left, :right)
  def to_s
    "#{left} < #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    if left.reducible?
      LessThan.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      LessThan.new(left,right.reduce(var_enviroment))
    else
      Boolean.new(left.value < right.value)
    end
  end
end

class GreaterThan < Struct.new(:left, :right)
  def to_s
    "#{left} > #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    if left.reducible?
      GreaterThan.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      GreaterThan.new(left,right.reduce(var_enviroment))
    else
      Boolean.new(left.value > right.value)
    end
  end
end

class Equal < Struct.new(:left, :right)
  def to_s
    "#{left} == #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    if left.reducible?
      Equal.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      Equal.new(left,right.reduce(var_enviroment))
    else
      Boolean.new(left.value == right.value)
    end
  end
end

class LessEqualThan < Struct.new(:left, :right)
  def to_s
    "#{left} <= #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    if left.reducible?
      LessEqualThan.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      LessEqualThan.new(left,right.reduce(var_enviroment))
    else
      Boolean.new(left.value <= right.value)
    end
  end
end

class GreaterEqualThan < Struct.new(:left, :right)
  def to_s
    "#{left} >= #{right}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    if left.reducible?
      GreaterEqualThan.new(left.reduce(var_enviroment),right)
    elsif right.reducible?
      GreaterEqualThan.new(left,right.reduce(var_enviroment))
    else
      Boolean.new(left.value >= right.value)
    end
  end
end

虚拟机的类也必须跟着改动:

class AbstractMachine < Struct.new(:statement,:var_enviroment)
  def step_next
    self.statement, self.var_enviroment = statement.reduce(var_enviroment)
  end

  def run
    while statement.reducible?
      puts "#{statement},#{var_enviroment}"
      step_next
    end
    puts "#{statement},#{var_enviroment}"  # final state
  end
end

下面可以测试变量定义了:

=begin
  x <= y + z
  4 <= y + z
  4 <= 2 + z
  4 <= 2 + 8
  4 <= 10
  true
  相当于:
  x = 4;
  y = 2;
  z = 8;
  puts x <= y + z;
  => true
=end
AbstractMachine.new(
   LessEqualThan.new(
     Variable.new(:x),
     Add.new(
       Variable.new(:y),
       Variable.new(:z))),
   {x: Number.new(4), y: Number.new(2), z: Number.new(8)}  
).run
  1. 语句

有了变量,那么变量还必须支持赋值,其实赋值本质就是在不停的修改变量上下文环境而已。赋值是一条语句,一旦求值就改变了抽象机器的状态,机器的状态目前就是变量上下文环境了,也就是那个Hash表。最简单的语句就是什么也不做,它不能规约,因为不会改变任何状态:

class DoNothing
  def to_s
    "do-nothing"
  end

  def inspect
    "#{self}"
  end

  def ==(other_statement)
    other_statement.instance_of?(DoNothing)
  end

  def reducible?
    false
  end
end

一个什么都不做的语句看起来没有什么意义,但是有时表示程序已经执行成功完毕会非常方便。所以在其他语句完成了它们的工作之后,我会将它们最终规约成DoNothing。赋值语句的规约非常好写,无非就是接收一个表达式并规约表达式到一个不可规约状态,最后把最终的规约的值拿来修改一个变量的值:

class Assign < Struct.new(:name,:expression)
  def to_s
    "#{name} = #{expression}"
  end

  def inspect
    "<<#{self}>>"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    if expression.reducible?
      [Assign.new(name,expression.reduce(var_enviroment)), var_enviroment]
    else
      [DoNothing.new, var_enviroment.merge({name => expression})]
    end
  end
end

手工对一个赋值表达式反复进行规约,直到终态:

# => x = x + 1
statement = Assign.new(:x, Add.new(
                            Variable.new(:x),
                            Number.new(1)))
# => {:x => 5}
var_enviroment = {x: Number.new(5)}

# => true
statement.reducible?

# => [x = 5 + 1, {:x=>5}]
statement, var_enviroment = statement.reduce(var_enviroment)

# => true
statement.reducible?

# => [x = 6, {:x=>5}]
statement, var_enviroment = statement.reduce(var_enviroment)

# => true
statement.reducible?

# => [do-nothing, {:x=>6}]
statement, var_enviroment = statement.reduce(var_enviroment)

# => false
statement.reducible?

通过抽象机器规约执行赋值语句:

=begin
  抽象机器支持赋值语句
  => x = x + 1, {:x=>5}
  => x = 5 + 1, {:x=>5}
  => x = 6, {:x=>5}
  => do-nothing, {:x=>6}
=end
AbstractMachine.new(
  Assign.new(:x, Add.new(Variable.new(:x),
                        Number.new(1))),
  {x: Number.new(5)}
).runs

到了现在,Simple语句还不完善,之前提到过,语言不支持控制流,因为有了bool型,那么现在就好实现if while循环这些特性了,另外还支持了代码块,一个代码块是由多条语句组成的:

class If < Struct.new(:condition, :true_statement, :false_statement)
  def to_s
    "if (#{condition}) { #{true_statement} } else { #{false_statement} }"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    if condition.reducible?
      [If.new(condition.reduce(var_enviroment), true_statement,false_statement),var_enviroment]
    else
      case condition
      when Boolean.new(true)
        [true_statement,var_enviroment]
      when Boolean.new(false)
        [false_statement,var_enviroment]
      end
    end
  end
end

#支持代码块
class CodeBlock < Struct.new(:first_statement, :second_statement)
  def to_s
    "{#{first_statement}; #{second_statement}}"
  end

  def inspect
    "#{self}"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    case first_statement
    when DoNothing.new
      [second_statement,var_enviroment]
    else
      reduced_first, reduced_enviroment = first_statement.reduce(var_enviroment)
      [CodeBlock.new(reduced_first,second_statement),reduced_enviroment]
    end
  end
end

=begin
  if(condition) {t = 7} else {t = 5}, {:condition => true}
  if(true) {t = 7} else {t = 5}, {:condition => true}
  t = 7, {:condition => true}
  do-nothing, {:condition => true, :t => 7}
=end
AbstractMachine.new(
  If.new(Variable.new(:condition),
        Assign.new(:t, Number.new(7)),
        Assign.new(:t, Number.new(5))),
  {condition: Boolean.new(true)}
).run

=begin
  if(x < y) {t = 7} else {x = 5}, {:condition => true, :x => 10, :y=>8}
  if(10 < y) {t = 7} else {x = 5}, {:condition => true, :x => 10, :y=>8}
  if(10 < 8) {t = 7} else {x = 5}, {:condition => true, :x => 10, :y=>8}
  if(false) {t = 7} else {x = 5}, {:condition => true, :x => 10, :y=>8}
  x = 5, {:condition => true, :x => 10, :y=>8}
  do-nothing, {:condition => true, :x => 5, :y=>8}
=end
AbstractMachine.new(
  If.new(LessThan.new(Variable.new(:x), Variable.new(:y)),
        Assign.new(:t, Number.new(7)),
        Assign.new(:x, Number.new(5))),
  {condition: Boolean.new(true), x: Number.new(10), y: Number.new(8)}
).run

=begin
  x = (2 + 5); y = (x + 3),{}
  x = 7; y = (x + 3),{}
  do-nothing; y = (x + 3),{:x => 7}
  y = (x + 3),{:x => 7}
  y = (7 + 3),{:x => 7}
  y = 10,{:x => 7}
  do-nothing,{:x =>7, :y=> 10}
=end
AbstractMachine.new(
  CodeBlock.new(
    Assign.new(:x, Add.new(Number.new(2), Number.new(5))),
    Assign.new(:y, Add.new(Variable.new(:x), Number.new(3)))
  ),
  {}
).run

=begin
  if (x < y) { {z = (2 + 5); z = (z + 10)} } else { w = 99 },{:x=>2, :y=>5}
  if (2 < y) { {z = (2 + 5); z = (z + 10)} } else { w = 99 },{:x=>2, :y=>5}
  if (2 < 5) { {z = (2 + 5); z = (z + 10)} } else { w = 99 },{:x=>2, :y=>5}
  if (true) { {z = (2 + 5); z = (z + 10)} } else { w = 99 },{:x=>2, :y=>5}
  {z = (2 + 5); z = (z + 10)},{:x=>2, :y=>5}
  {z = 7; z = (z + 10)},{:x=>2, :y=>5}
  {do-nothing; z = (z + 10)},{:x=>2, :y=>5, :z=>7}
  z = (z + 10),{:x=>2, :y=>5, :z=>7}
  z = (7 + 10),{:x=>2, :y=>5, :z=>7}
  z = 17,{:x=>2, :y=>5, :z=>7}
  do-nothing,{:x=>2, :y=>5, :z=>17}
=end
AbstractMachine.new(
  If.new(LessThan.new(Variable.new(:x), Variable.new(:y)),
         CodeBlock.new(
                       Assign.new(:z,Add.new(Number.new(2),Number.new(5))),
                       Assign.new(:z,Add.new(Variable.new(:z),Number.new(10)))
                       ),
        Assign.new(:w,Number.new(99))
        ),
  {x: Number.new(2), y: Number.new(5)}
).run

# 支持while循环

class While < Struct.new(:condition, :body)
  def to_s
    "while (#{condition}) { #{body} }"
  end

  def inspect
    "<<#{self}>>"
  end

  def reducible?
    true
  end

  def reduce(var_enviroment)
    [If.new(condition,CodeBlock.new(body,self),DoNothing.new),var_enviroment]
  end
end

=begin
  i = 1;
  result = 0;
  while(i <= 3)
  {
    result = result + 1;
    i++;
  }
  puts result; // result is 3
  while (i <= 3) { {x = (x + 1); i = (i + 1)} },{:i=><<1>>, :x=><<0>>}
if (i <= 3) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<1>>, :x=><<0>>}
if (1 <= 3) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<1>>, :x=><<0>>}
if (true) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<1>>, :x=><<0>>}
{{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<1>>, :x=><<0>>}
{{x = (0 + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<1>>, :x=><<0>>}
{{x = 1; i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<1>>, :x=><<0>>}
{{do-nothing; i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<1>>, :x=><<1>>}
{i = (i + 1); while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<1>>, :x=><<1>>}
{i = (1 + 1); while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<1>>, :x=><<1>>}
{i = 2; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<1>>, :x=><<1>>}
{do-nothing; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<2>>, :x=><<1>>}
while (i <= 3) { {x = (x + 1); i = (i + 1)} },{:i=><<2>>, :x=><<1>>}
if (i <= 3) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<2>>, :x=><<1>>}
if (2 <= 3) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<2>>, :x=><<1>>}
if (true) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<2>>, :x=><<1>>}
{{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<2>>, :x=><<1>>}
{{x = (1 + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<2>>, :x=><<1>>}
{{x = 2; i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<2>>, :x=><<1>>}
{{do-nothing; i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<2>>, :x=><<2>>}
{i = (i + 1); while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<2>>, :x=><<2>>}
{i = (2 + 1); while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<2>>, :x=><<2>>}
{i = 3; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<2>>, :x=><<2>>}
{do-nothing; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<3>>, :x=><<2>>}
while (i <= 3) { {x = (x + 1); i = (i + 1)} },{:i=><<3>>, :x=><<2>>}
if (i <= 3) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<3>>, :x=><<2>>}
if (3 <= 3) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<3>>, :x=><<2>>}
if (true) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<3>>, :x=><<2>>}
{{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<3>>, :x=><<2>>}
{{x = (2 + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<3>>, :x=><<2>>}
{{x = 3; i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<3>>, :x=><<2>>}
{{do-nothing; i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<3>>, :x=><<3>>}
{i = (i + 1); while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<3>>, :x=><<3>>}
{i = (3 + 1); while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<3>>, :x=><<3>>}
{i = 4; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<3>>, :x=><<3>>}
{do-nothing; while (i <= 3) { {x = (x + 1); i = (i + 1)} }},{:i=><<4>>, :x=><<3>>}
while (i <= 3) { {x = (x + 1); i = (i + 1)} },{:i=><<4>>, :x=><<3>>}
if (i <= 3) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<4>>, :x=><<3>>}
if (4 <= 3) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<4>>, :x=><<3>>}
if (false) { {{x = (x + 1); i = (i + 1)}; while (i <= 3) { {x = (x + 1); i = (i + 1)} }} } else { do-nothing },{:i=><<4>>, :x=><<3>>}
do-nothing,{:i=><<4>>, :x=><<3>>}
 
=end
AbstractMachine.new(
  While.new(
    LessEqualThan.new(Variable.new(:i),Number.new(3)),
    CodeBlock.new(
     Assign.new(:x,Add.new(Variable.new(:x),Number.new(1))),
     Assign.new(:i,Add.new(Variable.new(:i),Number.new(1))))
    ),
  {i: Number.new(1), x: Number.new(0)}
).run

以上代码使用小步语义的解决方式,使用代码块语句把while的一个级别展开,把它规约成一个只执行一次循环的if语句,然后再重复原始的while。

注意语句代码块支持两个语句,代码块可以嵌套,所以代码块支持多条语句组成的块。

  1. 正确性

如果程序支持语法有效但实际是错误的,那么按照之前写的代码,会崩溃,因为之前都没有考虑过一种情况:

=begin
    x = true; x = x + 1;
    do-nothing; x = x + 1, {:x => true}
    x = x + 1, {:x => true}
    x = true + 1, {:x => true}
    Error!
=end
AbstractMachine.new(
    CodeBlock.new(
        Assign.new(:x, Boolean.new(true)),
        Assign.new(:x, Add.new(Variable.new(:x),Number.new(1)))
    ),
    {}
)

以上代码是错误的,true 和 数值1不能相加。处理这个问题的一个方法就是在表达式能被规约的时候加入更多的约束,加入对求值失败可能性的考虑,这时求值过程可能终止,而不是总要试图规约成一个值,不然就可能崩溃,或者抛异常。

但是从实现者的角度考虑,我们可能不知道到参数的输入会是什么样子,加入这样的约束似乎很困难,从语法分析的角度是看不到这样的错误的,所以,最终,我们需要一个比语法更强大的工具,它能看到“未来”并让使用者,程序运行时避免执行任何可能崩溃或者终止处理的程序,这个就是大家可能听说过的静态分析。静态分析将采用一种静态语义(static semantics)的手段来根据语言的动态语义(dynamic semantics)来判断一个语法正确的程序是否具有有意义的含义。当然,这个就是语义分析。这篇文章到目前为止现在所讲解的都是程序的动态语义(操作语义)的内容,也就是说动态语义关注的是程序执行时具体做什么,到不关注程序这样做到底有没有意义。(这篇文章现在不会讲解静态分析的内容)

  1. 应用

到目前未知,Simple语言支持了变量定义,赋值,if,while等基本的语言特性,与其他工业界的主流语言远远不能相比,它还不支持函数定义,函数调用等等功能。因为其环境上下文仅仅只是变量的Hash表键值对,不具有更强大的表达式关联。

小步语义的细节化,面向执行过程的风格能让它无歧义地定义真实世界上的所有编程语言。例如,Scheme语言的R6RS标准就使用了小步语义描述其执行,比提供了PLT Redex语言对这些语义进行参考实现。PLT Redex是一种用了设计和调试操作语义的一门DSL。 Ocaml语言在一个更简单的Core ML语言上构建了一系列的分层,也有对基础语言运行时行为的小步语义定义。

大步语义

我们已经看到小步语义是什么样子了。就是设计一台抽象机器维护一些执行状态,然后定义一些规约规则,这些规则详细说明了如何才能对每种程序结构循序渐进的求值。这样的做法,大部分都有迭代的味道,它要求抽象机器反复执行规约步骤,这些步骤以及与它们同样类型的信息可以作为自身的输入和输出,这让它们适合这种反复进行的应用程序。

这种小步的方法有一个优势,就是能把执行程序的复杂过程分成更小的片段进行解释和分析,但它却是有点不够直接:我们没有解释整个程序结构是如何工作的,而只是展示了它是如何慢慢规约的。为什么不能更直接地解释一个语句,完整地说明它的执行过程呢?这正是大步语义(big-step semantic)的依据。相对于小步语义的局部解释和分析颇有些全局解释分析的味道。

大步语义的**是,定义如何从一个表达式或者语句直接得到它的结果。这必然需要把程序的执行当成一个递归而不是迭代的过程:大步语义说的是,为了对一个更大的表达式求值,我们要对所有比它小的子表达式求值,然后把结果结合起来得到最终答案。

在很多方面,这都比小步的方法更加自然,但确实失去了一些对细节的关注。例如,小步语义明确定义了操作应该发生的顺序,因为在每一步都明确了下一步规约应该是什么。但是大步语义经常会写成更为松散的形式,只会说哪些子计算会执行,而不会指明它们按照什么顺序执行。所以数学化地定义大步语义时,就不可避免地要讲清楚准确的求值策略了。

小步语义还提供给了一种轻松的方式用以监视计算的中间阶段的可能,而大步语义只返回一个结果,不会产生任何关于如何计算的证据。

为了理解这种权衡取舍,让我们回顾一些常见的语言结构,并看如何用Ruby来实现它的大步语义。我们之前的小步语义要求有一个AbstractMachine类来跟踪状态并反复执行规约,但是大步语义里,不需要这个类了。大步规约的规则描述了如何只对程序的抽象语法树访问一次就计算出整个程序的结果,因此不需要处理状态和重复。我们将只对表达式和语句类定义一个evaluate的求值方法,然后直接调用它。

  1. 表达式

处理小步语义时,我们不得不区分像1+2这样可以规约的表达式和像3这样不可规约的表达式,这样规约规则才能识别一个子表达式什么时候可以用来组成更大的程序。但是在大步语义中,每个表达式都能求值。唯一的区别,如果我们想要有个区别的话,就是对一些表达式求值会直接得到它们自身,而对另一些表达式求值会执行一些计算并得到一个不同的表达式。

大步语义的目标是像小步语义那样对一些运行时的行为建模,这意味着我们期望对于每一种程序结构,大步语义规则都要与小步语义规则程序最终生成的东西保持一致。从形式化上来说,就是把大小步语义这样的操作语义写成数学形式之后,这都是能被准确证明的。小步语义规则规定,像数值(Number)和布尔值(Boolean)这样的值不能在规约了,因此它们的大步规约非常简单:求值结果直接就是它们本身。

class Number < Struct.new(:value)
  def to_s
    value.to_s
  end

  def evaluate(var_eniroment)
    self
  end
      
  def inspect
    "<<#{self}>>"
  end
end

class Boolean < Struct.new(:value)
  def to_s
    value.to_s
  end

  def evaluate(var_eniroment)
    self
  end
      
  def inspect
    "<<#{self}>>"
  end
end

变量表达式是唯一的,这样它的小步语义允许它在成为一个值之前只规约一次,所以它的大步语义规则与小步规则一样,在变量环境中查找变量名,然后返回它的值。

class Variable < Struct.new(:name)
  def to_s
    name.to_s
  end

  def inspect
    "<<#{self}>>"
  end

  def evaluate(var_eniroment)
    var_eniroment[name]
  end
end

二元表达式Add,Multiply和Lessthan等等,它们要求先对左右子表达式递归求值,然后再用恰当的运算合并两边结果值:

class Add < Struct.new(:left,:right)
  def to_s
    "(#{left} + #{right})"
  end

  def inspect
    "<<#{self}>>"
  end

  def evaluate(var_eniroment)
    Number.new(left.evaluate(var_eniroment).value + right.evaluate(var_eniroment).value)
  end
end

class Minus < Struct.new(:left,:right)
  def to_s
    "(#{left} - #{right})"
  end

  def inspect
    "<<#{self}>>"
  end

  def evaluate(var_eniroment)
    Number.new(left.evaluate(var_eniroment).value - right.evaluate(var_eniroment).value)
  end
end

class Multiply < Struct.new(:left,:right)
  def to_s
    "(#{left} * #{right})"
  end

  def inspect
    "<<#{self}>>"
  end

  def evaluate(var_eniroment)
    Number.new(left.evaluate(var_eniroment).value * right.evaluate(var_eniroment).value)
  end
end

class Divide < Struct.new(:left,:right)
  def to_s
    "(#{left} / #{right})"
  end

  def inspect
    "<<#{self}>>"
  end

  def evaluate(var_eniroment)
    Number.new(left.evaluate(var_eniroment).value / right.evaluate(var_eniroment).value)
  end
end

class LessThan < Struct.new(:left, :right)
  def to_s
    "#{left} < #{right}"
  end

  def inspect
    "#{self}"
  end

  def evaluate(var_eniroment)
    Boolean.new(left.evaluate(var_eniroment).value < right.evaluate(var_eniroment).value)
  end
end

class GreaterThan < Struct.new(:left, :right)
  def to_s
    "#{left} > #{right}"
  end

  def inspect
    "#{self}"
  end

  def evaluate(var_eniroment)
    Boolean.new(left.evaluate(var_eniroment).value > right.evaluate(var_eniroment).value)
  end
end

class Equal < Struct.new(:left, :right)
  def to_s
    "#{left} == #{right}"
  end

  def inspect
    "#{self}"
  end

  def evaluate(var_eniroment)
    Boolean.new(left.evaluate(var_eniroment).value == right.evaluate(var_eniroment).value)
  end
end

class LessEqualThan < Struct.new(:left, :right)
  def to_s
    "#{left} <= #{right}"
  end

  def inspect
    "#{self}"
  end

  def evaluate(var_eniroment)
    Boolean.new(left.evaluate(var_eniroment).value <= right.evaluate(var_eniroment).value)
  end
end

class GreaterEqualThan < Struct.new(:left, :right)
  def to_s
    "#{left} >= #{right}"
  end

  def inspect
    "#{self}"
  end

  def evaluate(var_eniroment)
    Boolean.new(left.evaluate(var_eniroment).value >= right.evaluate(var_eniroment).value)
  end
end
  1. 语句

在我们要定义语句的行为时,大步语义就能真正发挥作用了。在小步语义下表达式会规约成其他表达式,但语句会规约成do-nothing并且得到一个经过修改的变量环境。我们可以把大步语义的语句求值看成一个过程,这个过程总是把一个语句和一个初始化的变量环境转成一个最终环境,这避免了小步语义不得不对reduce产生的中间语句进行处理的复杂性。例如,对一个赋值语句的大步规约方法应该是完整地对其表达式进行求值,并返回一个包含结果值得更新了的变量环境:

class Assign < Struct.new(:name,:expression)
  def to_s
    "#{name} = #{expression}"
  end

  def inspect
    "<<#{self}>>"
  end

  def evaluate(var_eniroment)
    var_eniroment.merge({ name => expression.evaluate(var_eniroment)})
  end
end

类似地,DoNothing的evaluate方法无疑将把未修改的变量环境返回,而If类的evaluate方法的工作相当直接:对条件求值,然后把变量环境返回,这个变量环境来自于对序列或者代替语句求值得到的结果。

class If < Struct.new(:condition, :true_statement, :false_statement)
  def to_s
    "if (#{condition}) { #{true_statement} } else { #{false_statement} }"
  end

  def inspect
    "#{self}"
  end

  def evaluate(var_eniroment)
    case condition.evaluate(var_eniroment)
    when Boolean.new(true)
      true_statement.evaluate(var_eniroment)
    when Boolean.new(false)
      false_statement.evaluate(var_eniroment)
    end
  end
end

class DoNothing
  def to_s
    "do-nothing"
  end

  def inspect
    "#{self}"
  end

  def ==(other_statement)
    other_statement.instance_of?(DoNothing)
  end

  def evaluate(var_eniroment)
    var_eniroment
  end
end

指称语义

到目前为止,我们已经从操作性方面观察了程序语言的含义,它通过展示程序执行之后发生的事情解释了程序的含义。而指称语义(denatational semantic)反而关心从程序的本来的语言到其他表示的转换。

这种类型的语义没有直接解释处理程序的执行,而是关注如何借助另一种语言(一般更低级,更形式化,至少比正在解释的语言更好理解)的已有含义来表示本来的语言。这样来看,这种语义可能近似于一种翻译器,但是,类似通常我们接触到的Less到CSS的转换,TypeScript到JavaScript的转换的理解。

指称语义是一种比操作语义更加抽象的方法,它不属于操作语义的范畴。它只是用一种语言替换成另一种语言,而不是像编译器或者解释器那样把一种语言转换成真实的行为。之前提到过,它更像一种翻译器,把中文转换为语义相同的英文,或者火星文。

指称语义通常用来把程序转化为更加形式化的数学对象语言,这样程序语言理论专家们就有了“共通的语言”来探讨问题了。

接下来我们需要把我们之前通过小步语义,大步语义定义的Simple语言转换到Ruby语言,实现Simple到Ruby的指称语义。

  1. 表达式

我们们可以用这个**为Number类和Boolean类写一个to_ruby的实现,意思就是把这个结构翻译成对应的Ruby结构,这两种结构在语义上是等价的。Number和Boolean都是值的本身。

# -> e { } 是参数为e的lambda表达式 是Ruby语言的lambda语法
class Number < Struct.new(:value)
    def to_ruby
        "-> e { #{value.inspect} }"
    end
   
    def inspect
      "<<#{self}>>"
    end
end

class Boolean < Struct.new(:value)
    def to_ruby
        "-> e { #{value.inspect} }"
    end
   
    def inspect
      "<<#{self}>>"
    end
end

如果在Ruby console上运行以上代码:

# => "-> e { 2 }"
Number.new(2).to_ruby
# => "-> e { true }"
Boolean.new(true).to_ruby

这些to_ruby的方法每个都产生一个刚好包含Ruby程序代码的字符串表示,并且因为Ruby是一种我们理解其含义的语言,所以可以直接可以利用已有的Ruby解释器来eval这些Ruby程序字符串。

# => 2
proc = eval(Number.new(2).to_ruby)
proc.call({})
# => true
proc = eval(Boolean.new(true).to_ruby)
proc.call({})

接下来,我们采用一致的**写出变量,加减乘除等。变量麻,还是类似Hash表的上下文环境,通过变量名来查找变量对应的值:

class Variable < Struct.new(:name)
    def to_ruby
        "-> e { e[#{name.inspect}] }"
    end
   
    def inspect
      "<<#{self}>>"
    end
end

# => "-> e { e[:test] }"
expression = Variable.new(:test)
expression.to_ruby
# => 6
proc = eval(expression.to_ruby)
proc.call({ test: 6})

加减乘除大于小于等操作符:

class Add < Struct.new(:left,:right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) + (#{right.to_ruby}).call(e) }"
    end
    def inspect
      "<<#{self}>>"
    end
end
  
class Minus < Struct.new(:left,:right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) - (#{right.to_ruby}).call(e) }"
    end
    def inspect
      "<<#{self}>>"
    end
end
  
class Multiply < Struct.new(:left,:right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) * (#{right.to_ruby}).call(e) }"  
    end
    def inspect
      "<<#{self}>>"
    end
end
  
class Divide < Struct.new(:left,:right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) / (#{right.to_ruby}).call(e) }"   
    end
    def inspect
      "<<#{self}>>"
    end
end

class LessThan < Struct.new(:left, :right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) < (#{right.to_ruby}).call(e) }" 
    end
    def inspect
      "#{self}"
    end
end
  
class GreaterThan < Struct.new(:left, :right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) > (#{right.to_ruby}).call(e) }" 
    end
    def inspect
      "#{self}"
    end
end
  
class Equal < Struct.new(:left, :right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) == (#{right.to_ruby}).call(e) }"
    end
    def inspect
      "#{self}"
    end
end
  
class LessEqualThan < Struct.new(:left, :right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) <= (#{right.to_ruby}).call(e) }" 
    end
    def inspect
      "#{self}"
    end
end
  
class GreaterEqualThan < Struct.new(:left, :right)
    def to_ruby
        "-> e { (#{left.to_ruby}).call(e) >= (#{right.to_ruby}).call(e) }" 
    end
    def inspect
      "#{self}"
    end
end

然后对以上代码进行测试:

# "-> e { (-> e { e[:a] }).call(e) + (-> e { 4 }).call(e) }"
Add.new(Variable.new(:a),Number.new(4)).to_ruby

# "-> e { (-> e { (-> e { e[:a] }).call(e) - (-> e { 2 }).call(e) }).call(e) > (-> e { 5 }).call(e) }"
GreaterThan.new(Minus.new(Variable.new(:a),Number.new(2)),Number.new(5)).to_ruby

# => 10
var_environment = { x: 6 }
proc = eval(Add.new(Variable.new(:x),Number.new(4)).to_ruby)
proc.call(var_environment) 

# => false
proc = eval(GreaterThan.new(Minus.new(Variable.new(:x),Number.new(2)),Number.new(5)).to_ruby)
proc.call(var_environment) 
  1. 语句

Assign给变量赋值无非还是修改Hash表上下文环境:

class Assign < Struct.new(:name,:expression)
    def to_ruby
        "-> e { e.merge({ #{name.inspect} => (#{expression.to_ruby}).call(e) }) }" 
    end
    def inspect
      "#{self}"
    end
end

# "-> e { e.merge({ :foo => (-> e { (-> e { e[:bar] }).call(e) + (-> e { 2 }).call(e) }).call(e) }) }"
statement = Assign.new(:foo, Add.new(Variable.new(:bar),Number.new(2)))
statement.to_ruby

# => {:bar=>6, :foo=>8}
proc = eval(statement.to_ruby)
proc.call({ bar: 6})

DoNothing啥也不做,一个啥都不做的lambda表达式:

class DoNothing
    def to_ruby
        "-> e { e }"
    end
end

最后程序代码块,逻辑控制if while语句:

class If < Struct.new(:condition, :true_statement, :false_statement)
    def to_ruby
      "-> e {" +
          "if (#{condition.to_ruby}).call(e)" + 
          "then (#{true_statement.to_ruby}).call(e)" +
          "else (#{false_statement.to_ruby}).call(e)" + 
          "end" + 
      "}"
    end
end

=begin
=> "-> e {          if (-> e { true }).call(e)          
then (-> e { e.merge({ :foo => (-> e { 3 }).call(e) }) }).call(e)          
else (-> e { e.merge({ :foo => (-> e { 6 }).call(e) }) }).call(e) end     }"
=end
If.new(Boolean.new(true),Assign.new(:foo,Number.new(3)),Assign.new(:foo,Number.new(6))).to_ruby
  
# => {:foo=>3}
proc = eval(
      If.new(
            Boolean.new(true),
            Assign.new(:foo,Number.new(3)),
            Assign.new(:foo,Number.new(6))
      ).to_ruby
  )
proc.call({})

class CodeBlock < Struct.new(:first_statement, :second_statement)
    def to_ruby
      "-> e { (#{second_statement.to_ruby}).call((#{first_statement.to_ruby}).call(e)) }"
    end
end

class While < Struct.new(:condition, :body)
    def to_ruby
        "-> e {" +
        "while (#{condition.to_ruby}).call(e);" +
        "e = (#{body.to_ruby}).call(e);end;" +
        "e" +  
        "}"
    end
end

=begin
=> "-> e {while (-> e { (-> e { e[:x] }).call(e) < (-> e { 3 }).call(e) }).call(e);
    e = (-> e { e.merge({ :x => (-> e { (-> e { e[:x] }).call(e) + (-> e { 1 }).call(e) }).call(e) }) }).call(e);
    end;e}"
=end  
  
Assign.new(:x,Number.new(0))
  statement = While.new(
      LessThan.new(Variable.new(:x),Number.new(3)),
      Assign.new(:x, Add.new(Variable.new(:x),Number.new(1)))
  )
statement.to_ruby

# => {:x=>3}
proc = eval(statement.to_ruby)
proc.call({x: 1})
  1. 指称语义的应用

指称语义为一种语言实现了到另一种语言的翻译器,它提供了一个基础标准可以检验源语言的语义实现。

早期版本的Scheme标准使用指称语义来定义核心语言,而不是像现在的标准使用小步语义来定义。

总结---形式语义实践

小步语义,大步语义,指称语义它们三都可以用来定义程序语言,前两者属于操作语义,最后一者不属于。

关于形式化语义的研究,之前文章描述的都不正式,唯一正式的就是看到了那些稀奇古怪的逻辑符号,让大家不明觉厉。这样写作的方式是为了用更加浅显易懂,接地气的方式让大家了解什么是程序的含义。

形式化语义的一个重要应用就是为一种程序语言的含义给出一个无歧义的定义,本质上我们之前写的小步语义就相当于用Ruby语言重写了Simple语言的小步语义形式化定义,简单来说就是用通俗的Ruby语言重写实现了那些稀奇古怪的逻辑符号描述的无歧义的定义。

操作语义跟解释器很像,就像王垠大神所说,语义学虽然表面上是定义程序语言的含义,但其实就是研究各种各样的解释器(因为只有解释器解释才有了含义),那些稀奇古怪的逻辑符号本质上就是解释器代码,只不过那些代码用了很难理解的数学化符号编写定义了一种程序语言罢了。

所以,由于操作语义跟解释器很像,那么计算机科学家就可以把一个适当的解释器看成一种语言的操作语义,然后证明它在那种语言的指称语义方面的正确性------这意味着证明了由解释器给出的含义和由指称语义给出的含义之间存在着明显的联系。

指称语义的一个优点就是比操作语义抽象层次更高,它忽略了程序如何执行的细节,而只关心如何把它转换为一个不同的等价表示,这样带来的好处就是程序语言理论专家可以对不同语言写成的两个程序进行比较研究。

形式化的指称语义使用的是抽象的数学对象来表示表达式和语句这样的编程语言结构,并且因为数学上的约定会规定如何对函数求值这样的事情,这就有了一种直接在操作意义上思考指称的方式。我们已经用了野路子,把指称语义看作是一种语言到另一种语言的翻译器,而事实上这是大多数程序语言最终得以执行的方式,毕竟无论经过多少步骤转换,它们最终是要翻译成机器码,被送入到CPU解释成微指令,然后微指令又被放到一个CPU核心上执行的,然后再逐步向下到硅和电子的物理世界。这些程序语言信息经过这样一步一步转换会在什么地方结束呢?

语义这个抽象的高层建筑会逐步到达底部暴露出实际的机器:这些机器是什么样子的? 可能是某种生物化学反应构造的计算装置,也可能是真正的量子计算机,也可能是现在目前造出的基于硅的半导体中的电子的物理机器,它们遵循的是物理法则。一台计算机是维护这个不确定结构的装置,大量复杂的解释层在彼此之上保持稳定。这就是计算理论有趣的地方------还是在说明抽象的重要性。

所以当我在谈论一种语言的语义或者某种语言特性时候,很少会提及它的实现,比如我现在已经很少提及C语言的局部变量是存放在栈空间的,malloc是在堆空间上分配的了,即使没有所谓的栈空间堆空间的概念了又怎样呢?难道我就不会C语言了吗? 所以那种时候,反而我更加关注一种语言特性它能干什么?是带来了好处还是坏处?我会更加关注一个程序语言的官方规范标准(Specification)。

如果你深入研究你会发现文章提及过的语义类型都有其他别称。小步语义还叫结构化操作语义(structural operational semantic)和转换语义(transition semantic)。大步语义更普遍的叫法是自然语义(natural semantic)和关联语义(relational semantic)。而指称语义还可以称为不动点语义(fixed-point semantic)或者数学语义(mathematical semantic)。

还有其他类型的形式语义,其中一个是公理化语义(axiomatic semantic),它通过在语句执行前后分别给出抽象机器状态的断言来描述一个语句的含义:如果一个断言(前置条件)在语句执行前初始化时true,那么随后的其他断言(后置条件)将是true。公理化语义在验证程序的正确性方面很有用:随着语句组合到一个更大的程序,它们对应的断言也可以组合成一个更大的断言,其目标就是表明对一个程序的总体断言与程序编写者的预期定义匹配。看到这里如果了解过程序语言理论的人脑袋可能会想起一种叫霍尔逻辑(Hoare logic)的概念。

公理化语义确实与这个相关。如果想了解,我就摘抄王垠大神写的《智能合约和形式验证》文章中的说法:

我好像已经把你搞糊涂了…… 我们先来科普一下 Hoare Logic。Hoare Logic 是一种形式验证的方法,用于验证程序的正确性。它的做法是,先给代码标注一些“前条件”和“后条件”(pre-condition 和 post-condition),然后就可以进行逻辑推理,验证代码的某些基本属性,比如转账之后余额是正确的。

举一个很简单的 Hoare Logic 例子:
{x=0} x:=x+1 {x>0}

它的意思是,如果开头 x 等于 0,那么 x:=x+1 执行之后,x 应该大于 0。这里的前条件(pre-condition)是 x=0,后条件(post-condition)是 x > 0。如果 x 开头是零,执行 x:=x+1 之后,x 就会大于 0,所以这句代码就验证通过了。

Hoare Logic 的系统把所有这些前后条件和代码串接起来,经过逻辑推导验证,就可以作出这样的保证:在前条件满足的情况下,执行代码之后,后条件一定是成立的。如果所有这些条件都满足,系统就认为这是“正确的程序”。

关于用这些形式验证系统,已经有很多实际应用了,因为有些领域确实需要安全性比较高的代码,一不小心除了问题就是大事,所以会非常注重程序代码的正确性,比如windows的驱动,高铁动车的调度系统,CPU的设计,还包括一些金融机构的核心交易系统都涉及到了形式验证来证明代码的正确。其中windows的驱动就有一种安全标注语言叫SAL,就是霍尔逻辑的一个实现。

实现语法分析器

学过编译原理这门课程的人都知道,实现一个编译器或者解释器都需要首先从词法分析,语法分析入手,到后面才是文章提到的语义分析相关的内容。为什么我可以直接开始讲语义部分呢?因为我们的Simple语言直接就是用类似抽象语法树的结构来编写的,类似于Lisp的代码结构:

(print 
  (+ 3 5)
  )

这种类似树型的嵌套结构已经非常接近抽象语法树了,我们可以直接省略语法分析了,直接进入最重要的语义部分,这个部分涉及到程序变换,代码验证,代码优化,这是编译器的中端部分,这才是编译器和程序语言理论的精华。

编译器往往涉及了前端(词法分析,语法分析),中端(语义分析,类型系统,程序变换),后端(机器码生成,优化)。我认为现在中端对于我这篇文章才是最重要的,前端的实现涉及到状态机,各种文法理论,递归下降分析等各种分析方法固然是编程能力的体现,但是相对于中端就不是那么重要了,有现成的工具可用。后端也有现成的工具可用,比如LLVM,Rust语言Swift语言的后端就是直接采用LLVM的。LLVM是当今最好的编译器后端框架。

编译器前端框架就多了,各种Parser Generator。比如,ANTLR,Bison, Lex & Yacc。 这些工具只要你写下自己设计的语言的BNF文法,就能帮助你生成对应的语法分析器了。如果你想自己手写词法分析和语法分析,那非常苦逼,恐怕你自己得补习下前端的相关理论了。这篇文章不会涉及。中端有没有相应的框架呢? 没有,大部分中端的内容其实相当理论,要了解前沿恐怕需要去读论文。即使有相应的框架可能也只是涉及皮毛,解决不了多数问题,最重要也没有很著名的框架工具。知道为什么没有框架吗?因为前端后端已经是发展极为成熟的领域了,前端理论快被压榨干了,后端大部分都是机械化的处理数据,优化,生成。很难再出现什么新鲜的玩意了,虽然很复杂很难,但是估计也就那样了。中端到现在还有各种各样有趣的东西,各种语言特性,语义,类型系统都在这里被设计,是程序语言理论学家们研究的领域。各种论文层出不穷,各种方法理论,固然有换汤不换药的理论论文出来导致重复,但是还是一个正在发展的领域。

好了,这篇文章到此结束,我会写一个类似的系列。下一篇会讲自动机的理论,这涉及到词法分析的理论,也同样用Ruby演示。:)

bat常用变量整理


title: bat常用变量整理
date: 2014-02-20 17:49:00
tags:

  • windows

@echo off

  • echo 当前盘符:%~d0
  • echo 当前盘符和路径:%~dp0
  • echo 当前批处理全路径:%~f0
  • echo 当前盘符和路径的短文件名格式:%~sdp0
  • echo 当前CMD默认目录:%cd%
  • echo 目录中有空格也可以加入""避免找不到路径
  • echo 当前盘符:"%~d0"
  • echo 当前盘符和路径:"%~dp0"
  • echo 当前批处理全路径:"%~f0"
  • echo 当前盘符和路径的短文件名格式:"%~sdp0"
  • echo 当前CMD默认目录:"%cd%"

参考:

什么是类型安全?


title: 什么是类型安全?
date: 2017-03-16 15:30:48
tags:
- 类型系统
- 程序语言理论

什么是类型安全?

有时候有些人说,Java是个类型安全的语言,那么这些人到底是在说什么?所有的类型安全的语言是一样的吗?

事实上,一个语言类型安全的定义是取决于语言类型系统的定义。简而言之,类型安全能保证程序的行为是意义明确的(well-defined)。更广泛的来说,我将要讨论的话题就是,一个语言的类型系统是推导程序正确性,安全性的一个有利工具。类型系统也是程序语言理论一个热门的研究领域。

思考下,为什么动态类型的语言,比如,Python,Javascript这些语言没有良好的代码提示工具呢?也为什么没有良好的函数定义跳转工具呢?

其实就是由于类型系统的影响,导致没有工具能非常正确分析程序结构,导致跳转,提示不正确。简单的程序可能可以分析出来,但是一旦程序结构复杂了,就难以作出代码的正确提示。所以为了弥补Javascript的不足,才有了静态类型的TypeScript,这样静态类型的语言在语言编译的时候就可以借助编译器或者静态分析工具分析出大量隐含的错误(警告),也更利于为其开发良好的代码提示,重构工具,也更利于大型项目的开发维护。好处是不言自明了。

基本的类型安全

类型安全基本可以用一句话总结:有良好的类型系统的语言能保证自身程序不出错。

这句话其实不是我创造的,是来自于Robin Milner他1978年的一篇论文,论文叫《A Theory of Type Polymorphism in Programming》。翻译过来就是,编程中的类型多态理论。

好,下面我们来逐步讲解这句话是什么意思。

1. 程序出错

编程语言是由其语法和语义定义的,语法就是程序该怎么写。 语义就是程序的表述意义。

其实对于现实中的语言来讲,很多表达方法都是语法正确,但语义存在问题的。

就拿一个顶级语言学家乔姆斯基(Chomsky)经典的英文例子来说,Colorless green ideals sleep furiously。这句英文语法上是完全正确的,但是句子表述的意义本身毫无意义,无颜色的绿色是什么鬼?

再来一个Ocaml编程语言的例子,1 + "foo" , 根据Ocaml语言的语义这个句子毫无意义,数值和字符串做加法是什么鬼?

还有个C语言的例子:

char buffer[5];
buffer[5] = 'F';

以上代码语法完全正确,但是数组的下标越界了,在C语言规范中,这样的写法是未定义行为(undefined bahavior)。这是毫无意义的,基于该语句所导致的任何后果现象作出讨论都是无意义的,程序可能崩溃,也可能看上去“运行良好”,但是实际上已经错了。无意义的程序就是错误。类型系统就是为了对这样的行为做出约束。

2. 良好的类型系统可以保证程序不出错

在类型安全的语言中,其类型系统能保证程序的正确运行,如果说,一个语言的类型系统能保证程序不出错,我们就可以说这个语言是良好类型的(well-typed)的。一个well-typed的语言肯定是well-defined的语言。
well-defined包含well-typed,well-typed包含于well-defined。

在类型安全的语言中,well-typed的语言是well-defined语言的子集,它们都是语法正确的语言的子集。

all langauge > well defined language > well typed language

什么语言才是类型安全的?

我们来看看几种流行的语言是否是类型安全的。

1. C/C++

非类型安全,C语言的类型系统不对无意义的行为做约束,例子数组越界。而C++可以认为是C语言的超集(为了兼容垃圾C语言这个历史包袱不得不作出的妥协),也没有对数组下标越界作出约束,所以也是非类型安全的。当然还有为了兼容C,C++允许随意的强制类型转换很容易破坏类型系统。所以更加类型安全的类型转换,dynamic_cast, static_cast, const_cast等。

2. C#/Java

可能是类型安全的,因为很难通过观察对一个成熟的语言实现作出断言。例如,早期的Java版本泛型的类型推导是不正确的。当然一种叫Featherweight Java的方言就是类型安全的。至于为什么,可以告诉你,这是理论证明出来的。

有趣的是,满足类型安全的一个要点就是,C语言的语义没有对数组越界作出约束,而C#和Java对于数组越界统统会抛ArrayBoundsException的异常。

3. Python/Ruby

它们是否是类型安全的值得商榷。Python和Ruby往往被人称作动态类型(也被称作鸭子类型)语言,它们在执行中如果发生类型错误,就会抛出异常。就像Java对于数组越界在运行时会抛出异常,Ruby也一样,在运行时如果进行一个整数和字符串类型的加法也会抛异常。这样的行为是被语言的语义所规定约束的,所以它们都有良好的定义(well-defined)。

事实上,正是语言的语义赋予程序以意义。所以就本身而言,这些语言都是类型安全的,这种类型安全依赖于一种"无类型"系统(null type system),因为它能接收任何程序并且不让程序出错稳定运行。因此,是类型安全的。

这个结论看样子有点奇怪。在Java中,如果一个程序, object.method()被视为well-typed。那么类型安全会保证object是一个真正合法的对象,所以对method方法的调用总会成功。如果换做Ruby的话,那么object.method()依赖于Ruby的null type system保证是well-typed,虽然当我们运行Ruby的这段程序(没有任何保证object对象确实定义了一个叫method的方法),这段程序要么成功,要么会抛出异常,它确实会正常运行下去。如果是Java直接在编译时就报错了。

所以简短来说,类型安全不是万能的,类型安全所作出的保证是依赖于语言的语义的,语义隐式定义了语言的错误的行为(wrong behavior)。在Java中,这样调用一个不存在的方法就是错误的行为,在Ruby中,就不是错误的,它仅仅是抛出一个异常。

深入点的探讨

一般来说,类型安全确确实实是有用的,如果没有它,我们就无法保证程序按照我们的意图,并像它们所定义的那样运行下去,这样程序就能做一些非法的事了。

C/C++对未定义行为作出了一定程度的忍让,所以这也让C/C++编写的程序是导致很多漏洞,软件攻击的根源,比如stack smashing,利用栈溢出进行攻击, 再比如格式化字符串攻击。这种类型的漏洞和攻击是不可能发生在类型安全的语言编写的软件中的。

上面只是简单对Java和Ruby作出了一定的阐述。下面来对类型系统做一些更深入的探索。不是所有语言的类型系统都一样,有些类型系统能保证的指标,其他类型系统却不能保证。所以,我们在探索一门语言的时候,不要只确认它是否是类型安全的,而是确认这个类型系统满足了哪些指标,哪些指标是你所关心的,哪些不是。 废话了这么多,下面开始吧。

1.缩小鸿沟

之前文章提到过,well-defined > well-typed,well-defined的语言不一定是well-typed。所以这两者类型系统之间有什么区别呢?从well defined到well typed的过渡是什么情况?先说结论,这类过渡的语言都是well defined的,但是这类语言的类型系统不会“拒绝”,举个例子,对于大多数类型系统会“拒绝”下面一段程序:

//虽然是javascript的语言的语法,但是不要理解成js语言,这里
//只是为了方便表述**
function test(p){
 var x;
 
 if (p) x = 5;
 else x = "hello";
 if (p) return x+5;
 else return strlen(x);
}

这个函数,无论p的值是true,还是false,该函数都会返回一个int类型。
但是如果是非well typed语言的类型系统就不会会“拒绝”这段程序,而well typed语言的类型系统会“拒绝”这段程序。因为变量x的类型“同时”是String类型和int类型,用类型推导的理论就是,bool -> int ^ bool -> String,这种表示的是intersection type,而不是union type。intersection type表示的是变量x“同时”是String和int类型,而不是“有时”是String类型,“有时”是int类型。

再举个类似的例子:

function test(param){
    return param;
}

var a = test(1);
var b = test(true);
var c = test("Hello");

以上的函数test类型就是intersection type的,bool -> bool ^ String -> String ^ int -> int 。也就是函数的输出输入可以"同时"是这三种类型。如果是Union Type,那么调用test(1)就会报错,因为test函数类型还有可能是bool -> bool , String -> String。 调用test(true),test("Hello")都会出错,原理一样的,左右不是人。垠神的文章也解释过。

回过头来看第一段程序,根据静态分析工具其语言的类型系统是正确但不完备的。这不完备性也让很多程序员失望。一个补救的措施就是,让其类型系统能够处理,缩小well-defined与well typed的鸿沟。

给一个参考,JavaScript是无类型的语言吗?

一个例子,Java的类型系统在1.5版本的时候引入了泛型的概念,在1.4中,你需要类型转换来告诉java的类型系统来接受程序,1.5就不需要转化了。另一个例子是lambda演算,函数式编程语言的基础,lambda演算其中一个加入类型的版本叫Simply Typed lambda calculus,这个类型系统相对于Milner’s polymorphic type system能接受更少的程序,而Milner's type system又比支持Rank-2 (or higher) polymorphism的类型系统接受更少的程序。

所以设计一个完备,表现力强大,可用的类型系统是程序语言理论的热门领域。

2.强制不变性

一些你所见过的经典编程语言都有类型声明,int,string等等。类型安全可以保证表达式的类型必须与类型所声明的一致。不必限于int类型,一个类型系统可以支持更加广泛丰富的类型,从而表达程序更加有趣的特性。

比如在研究领域一个细化类型(refinement types)有趣的例子,就是利用逻辑公式描述一个类型一组的可能的值。类型{v: int | 0 <= v},用公式0 <= v细化了int类型,它简洁高效的定义了非负整数类型。细化类型允许程序员为数据结构的类型表达数据结构不变性,类型安全正是由于这些不变性保证的。细化类型系统已经有实例了,比如,在Haskell中的Liquid Haskell,F#中的F7

另一个例子,我们可以用类型系统(通过强制共享数据不变性),来防止数据竞争(data race)。这个情况下,那么共享变量的类型本质上就是一个锁来保护它的不变性。这样叫Types for safe locking第一次是在Abadi 和Flanagan的文章中第一次被提及。 这个特性有Java版本的实现,戳这里。也有C语言版本的实现

当然还有很多使用有趣类型系统的例子,限制被污染数据的使用防止私有信息的泄露等等。

3.类型抽象和信息隐藏

许多编程语言都允许数据抽象(也叫信息隐藏)。这些语言提供了一些抽象手段,比如提供类(classes),模块(modules),函数(functions)这样的概念。它们可以把内部的结构不暴露给使用者,面向接口编程,接口不变,实现变化。

类型系统在这样的抽象中就扮演至关重要的角色。表述独立(Representation independence)说明了程序的运行只应该依赖其抽象,不应该依赖其实现。之后的John Reynolds在类型和抽象上又做出了开创性的工作,发表于他1983年的一篇paper,用一句简单的话来概括这论文就是,类型结构是维护抽象级别的一个语法工具,也就是说,类型是构建可维护系统的最基础重要(fundamental)的角色。

所以!!!!

为啥Haskell写出来的代码在懂的开发人员面前更好维护? 为啥Haskell编译通过的代码基本能保证正确?? 为什么需要给动态类型语言做静态分析工具?? 为什么静态类型的语言有更智能的代码提示和重构工具?? 为什么基于动态类型的语言开发的没有特别大型的开源项目??为什么要在javascript之上还要再开发一个TypeScript语言用来支持大型项目的开发?? 难道是闲着蛋疼?? 所有的所有,都是因为,程序是类型上的证明。

参考资料

manjaro下安装配置MySQL8


title: manjaro下安装配置MySQL8
date: 2018-08-25 14:13:47
tags:
- MySQL
- Linux

虽然Arch体系的Linux都推荐用MariaDB,也不知道为什么Arch Linux不大推荐MySQL,虽然二者协议兼容,但是我就想用MySQL。

我这边是中科大的源。

sudo pacman -S mysql
sudo mysqld --initialize --user=mysql --basedir=/usr --datadir=/var/lib/mysql

如果成功,terminal会出现:

2018-08-25T05:33:04.399546Z 0 [Warning] [MY-010915] [Server] 'NO_ZERO_DATE', 'NO_ZERO_IN_DATE' and 'ERROR_FOR_DIVISION_BY_ZERO' sql modes should be used with strict mode. They will be merged with strict mode in a future release.

2018-08-25T05:33:04.399603Z 0 [System] [MY-013169] [Server] /usr/bin/mysqld (mysqld 8.0.12) initializing of server in progress as process 20610

2018-08-25T05:33:40.201045Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: 56%ei0#nQ:/;

2018-08-25T05:34:11.553951Z 0 [System] [MY-013170] [Server] /usr/bin/mysqld (mysqld 8.0.12) initializing of server has completed

临时产生了一个root用户的密码,需要登录进去重新更改才可以操作数据库。

先启动mysql服务:

sudo systemctl start mysqld

然后登录:

mysql -u root -p[temp_password]

更改root临时密码:

ALTER user 'root'@'localhost' IDENTIFIED BY 'new_password';

然后就可以各种命令了, 建库,建用户,授权等等:

create database [new_db_name];

create user 'new_user_name'@'localhost' identified by 'user_password';

use [db_name];

grant all privileges on [db_name].* to '[user_name]'@'localhost' with grant option;

flush privileges;

EOF

manjaro折腾手记


title: manjaro折腾手记
date: 2018-08-11 09:00:00
tags:

  • Linux

青葱岁月,一去不复返啊

前言

对于Linux呢,我自己算不上熟悉,但也不是很陌生。大一上学期的时候在对于操作系统什么都不懂的情况下,废了九牛二虎之力装了个ubuntu。随便敲了下命令行,于是便草草了事。

工作了以后,使用的开发环境windows和Linux都接触过,不过对于windows接触的是最多的。因为大学主要业余写的是win32的代码。工作的时候呢,如果遇到程序需要运行在Linux上,大部分的时候是在windows上用IDE写好代码,然后配置好cmake,然后cmake生成Linux下的GNU Makefile文件,就可以make工程了。因为工作的稳定需要,接触最多的Linux发行版是CentOS,RedHat,ubuntu,当然还有Fedora和OpenSUSE。

所以对于Linux说实话,还是没有依赖得过多,它对于我来说,不过就是个工具。我没有像其他人那样对某个发行版的热爱上升到信仰程度。但是由于网络上某些人的安利,曾经试着装过具有高度自由,高度灵活的Linux发行版---Arch Linux

当然,对于刚开始接触Arch的我,完全通过命令行安装Arch的过程,还是把我折腾的够呛,不过最后还是装上了。之后,在上面写了个hello world就把Arch卸载了。没用过Arch社区提供的pacmanAUR

但是最近工作的原因,需要完全在Linux下工作更加方便,所以把家里这台个人电脑的windows全部格了,因为考虑到我这台电脑性能的问题,没有装Deepin Linux,Deepin UI太炫了,占用资源高,听社区反映有些时候桌面环境会卡死,于是索性就装了个manjaro Linux

选择manjaro的几个原因如下:

  • Arch的衍生版,继承自Arch几乎所有优点(软件源更新非常快,软件时常保持最新,内核滚动升级,利用AUR和pacman的包管理)。

  • 图形化的安装界面,一路Next,几乎开箱即用。

  • 硬件支持非常好。

  • 高度可定制化,而且Xfce的DE,资源占用低。

折腾手记

更新系统

因为manjaro是滚动发布的,所以安装完成重启后,进入系统第一件事情最好是打开terminal,运行以下命令:

sudo pacman -Syu

目的是更新本地的源数据库和更新系统。不然如果长时间不升级系统,容易滚挂掉。不过听说manjaro对于Arch很少发生这种意外,非常稳定。

通过NTP服务设置当前系统时间

安装完成的系统跟当前时区的时间不同步,采用NTP协议来同步时间:

sudo ntpdate cn.ntp.org.cn #可以换成任何一个公开的NTP Server地址

安装yaourt

yaourt是pacman的一个外壳,同时它还支持AUR。AUR是一个Arch Linux用户的软件仓库,也就是说,很多用户可以把自己喜欢的软件包上传上去,分享给其他用户。我们在AUR里面搜索得到的软件,可以通过yaourt来简化安装流程。

如果不用yaourt,你恐怕得手动编译软件的很多东西,要多敲十几个命令行,不划算。yaourt这些步骤都对用户透明化了。方便不少。不过这里提示一点,AUR里面的软件毕竟是野生的,没有经过完全测试,没有官方源提供的安全,所以有一定风险,安装一个软件前,需要仔细评估,查看用户评论。但是Arch软件巨多,就是强大在AUR上面了。

运行一下命令就安装yaourt了:

sudo pacman -S yaourt

之后你就可以通过以下命令搜索AUR上的软件了:

yaourt -Ss [package name]

安装Visual Studio Code

毕竟我是微软的粉丝,最近几年微软大力支持开源。VSCode真是良心的开源跨平台的编辑器产品,获得网络上的一致好评,在windiws下用它也用的多。反正Linux下的vi vim nano这些用得不顺手,也不熟悉。

直接用yaourt安装VSCode:

yaourt -S visual-studio-code-bin

安装中文输入法

然后就是安装中文输入法--搜狗输入法。Deepin自带了,毕竟国产Linux。但是manjaro可没这么好。

  1. 首先添加国内源

打开/etc/pacman.conf配置文件。并在文件末尾添加:

[archlinuxcn]
SigLevel = Optional TrustedOnly
Server = https://mirrors.ustc.edu.cn/archlinuxcn/$arch
  1. 更新源并导入GPG Key
sudo pacman -Syy && sudo pacman -S archlinuxcn-keyring
  1. 安装搜狗输入法
sudo pacman -S fcitx-im #默认全部安装
sudo pacman -S fcitx-configtool # 图形化配置工具
sudo pacman -S fcitx-sogoupinyin
  1. 编辑添加输入法配置文件

生成并编辑~/.xprofile文件,添加以下内容:

export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
export XMODIFIERS="@im=fcitx"
  1. 重启系统,使输入法安装生效
  2. 通过fcitx-configtool配置工具添加搜狗输入法
  3. 完成

安装CLion

喜欢JetBrain系列的IDE,没有IDE简直没法干活,windows上当然是Visual Studio,但是Linux上只能是CLion了。当然,感受了下,它同样强大,作为Linux下的C++ IDE,它够用了,支持CMake工程模型。

sudo yaourt -S clion
sudo yaourt -S clion-cmake
sudo yaourt -S clion-gdb
sudo yaourt -S clion-jre

安装Adobe Flash Player Plugin

这样最起码可以看bilibili了,= =。

sudo pacman -S flashplugin

安装迅雷

我发现Linux下的aria2这个下载神器堪比迅雷,我下载aria2,并配置了aria2.conf中的tracker,还是不行,下载几乎没啥速度。而且aria2也不支持ed2k。而在Linux上支持ed2k的amule客户端下载速度也是以0计算。太烂了,也许不能开箱即用吧。

想想还是迅雷好,国产Deepin已经通过deepin-wine已经完美移植迅雷过来了。所以我们安装下就可以了。

sudo pacman -S deepin-wine
sudo pacman -S deepin.com.thunderspeed

这里我感谢武汉深之度的程序员日日夜夜的努力,这种造福人类的事情值得表扬。当然安装上迅雷有点BUG,就是中文字符显示不出来。我只能猜着点击按钮下载,不过好在界面布局我比较熟悉,下载没出啥意外。电影天堂的大部分ftp电影都能下载下来。

影音娱乐

音乐就听听网易云的Web版本了。电影的话,manjaro自带VLC,这个极度强大,但是界面不好看。把它卸载了。

sudo pacman -Rss vlc

换了deepin出品的deepin-movie。

sudo pacman -S deepin-movie

推荐些命令行神器

话说,工欲善其事必先利其器。

方便好用,我也安装了,这个也是收集大部分网友的答案。

  • ag,比grep、ack更快的递归搜索文件内容。

  • shellcheck, shell脚本静态检查工具,能够识别语法错误以及不规范的写法。

  • htop, 提供更美观、更方便的进程监控工具,替代top命令。

  • glances, 更强大的 htop / top 代替者,做综合监控。在某些方面,并没有htop好。

  • ncdu, 可视化的目录占用空间分析程序

  • pandoc, 可以将 markdown 转成各式各样的格式:PDF、DOCX、EPUB、MOBI,我通产用它生成PPT和简历。

  • Graphviz,可以将文本转化为图表

  • httpie, 测试http接口的时候,感觉比curl简单。curl毕竟更底层些,但是curl却更加灵活。

不要看不起手工拖控件画UI


title: 不要看不起手工拖控件画UI
date: 2013-07-18 19:56:00
tags:
- 软件工程

相比手动代码+脑内模想完成ui的方式,拖控件没什么不好

  • 拖控件的过程实质是在约束环境中填补某框架数据。同一水平的生产者(新增bug的概率一致),基于成熟框架之上二次开发(拖控件),相比“手工垒代码方式”,其新增bug的概率只会减不会增。从而:

    • 可间接提升模块质量
    • 模块质量要求不变时,可降低从业者门槛,以至于招人方便点,且可恰如其分的使用雇员能力
  • 拖控件只涉及UI,不会导致所谓的“应用技术层次下降”,换句话说:

    • 一个能靠拖控件弄出完全一致界面的应用,其界面部分的技术层次本就不能算高
    • 界面如何完成,与应用内部其他部分的实现难度无关。即便界面使用拖控件方式完成,高难度的应用仍具有高难度
  • 约束的开发环境,容易成为高开发效率的整体开发模型的一部分,从而在整体实施这套模型后,提升开发效率

  • 拖控件方式便于完成原型,不需要花不必要的功夫在代码上

PS:上述内容暗示,拖控件的方式是有利于软件开发工业化的,不针对自己捣鼓的东西,那些东西爱怎么弄怎么弄。

软件设计中的逻辑等价


title: 软件设计中的逻辑等价
date: 2016-04-28 09:29:14
tags:
- 软件工程

在系统软件设计中,人们常常为了软件(框架)的灵活性,扩展性,实现外部动态可配置。经常会使用配置文件初始化软件的逻辑,随着软件规模的越来越大,配置文件也越来越复杂,当复杂到一定程度,配置文件里面的文法表述甚至成了种奇葩的配置描述语言,如果不用外部配置,那么软件内部必须实现相应的逻辑,如果想让软件内部降低复杂性,那么隐藏的逻辑又会转移到配置文件,这种像极了物理中的能量守恒,同样软件中的逻辑是守恒的,以不同的形式表述,复杂性只能分散,不能消除。当然还可以把逻辑扔给用户输入,不过复杂性又会转移到软件说明手册或者是企业的技术支持上去了。No Silver bullet。书籍<<梦断代码>>也在讲述这个客观事实,无论多么牛逼的工程师都会被这事实头疼困扰。软件开发就是如此艰难,进展缓慢。

软件的未来


title: 软件的未来
date: 2016-08-05 13:55:27
tags:
- 软件工程

开源

开源社区,开源软件的发展大大减少的程序员的劳动力,降低了生产门槛。随着这几年开源运动的发展,GitHub,StackOverflow的出现,以及各大厂商拥抱开源以后,软件开发变成了软件的组装,开源组件(系统)降低了软件的研发成本,让超小的研发团队就能控制超大的系统软件。也正是因为这样,才像雨后春笋一般催生了各种互联网的创业中小型公司。

软件开发模型

软件的开发变得无比敏捷,从以前的瀑布模型到现在的增量式迭代开发,持续集成,持续交付,容器微服务等这些工程实践,让软件的发布速率提高了几个数量级。

数据

数据的地位慢慢超过了算法,因果关系变得不再重要,智能硬件机器的大规模普及产生了很多数据,从大数据中挖掘出相关性就可以分析出无法估量的潜在价值。10年前Google的GFS,MapReduce,BigTable这三篇论文打开了大数据的大门,而现在Hadoop,Apache Spark是互联网数据分析的标配,大量的数据也催生了刚开始快速发展的机器学习和深度学习。

云计算

云计算的普及,资源的虚拟化让无数创业公司形成了可能,进一步降低了运营成本。软件的架构变化从单机逐渐转向到了多机的分布式集群。

Web

随着V8引擎的出现,javascript性能的大幅度提升,软件不再是Native App,而是Web App或是Hybird。Web应用越来越移动化。Html5 CSS3的发展使得Web页面更加细腻丰富,带来了响应式网页的概念,同时更好的适配多种设备。

编程语言

编程语言越来越多样化,软件越来越平民化,编程的门槛大幅度降低,很多人经过简单的培训,自学就可以胜任开发工作。今后,更多的DSL出现,企业自动化,信息化的发展让各领域的人都需要编程的技能。

一种简单的C++错误码和描述定义方式


title: 一种简单的C++错误码和描述定义方式
date: 2015-11-09 11:04:42
tags:
- C/C++

我们平时有这样的需求,可能是C语言用户的老习惯了,在底层的组件中更喜欢用返回错误码的形式来告知函数的调用结果,一般来说,简单用#define 一个宏来包装下返回值。

#define ERR_SYSTEM_INIT -23 // system initailized fail

以上定义了一个错误码返回-23,意味着系统初始化失败。但是宏包含的信息太少,有些时候,用户不能知道错误的详细原因。必须给这错误加以说明。所以就索性写了一个Error类可以定义错误码和相关错误描述信息,并通过类似于Win32 API GetLastError这样的函数让错误码返回更详细的说明:

#include <string>
#include <map>
#include <cassert>

class Error
{
  public:
   Error(int value, const std::string& str)
   {
     m_value = value;
     m_message =    str;
#ifdef _DEBUG
     ErrorMap::iterator found = GetErrorMap().find(value);
     if (found != GetErrorMap().end())
     assert(found->second == m_message);
#endif
     GetErrorMap()[m_value] = m_message;
   }

// auto-cast Error to integer error code
   operator int() { return m_value; }
  private:
    int m_value;
    std::string m_message;

    typedef std::map<int, std::string> ErrorMap;
    static ErrorMap& GetErrorMap()
    {
       static ErrorMap errMap;
       return errMap;
    }

 public:

    static std::string GetErrorString(int value)
    {
       ErrorMap::iterator found = GetErrorMap().find(value);
       if (found == GetErrorMap().end())
       {
          assert(false);
          return "";
       }
       else
       {
          return found->second;
       }
    }   
};

以下是该类的使用方法:

#include <isotream>
#include "ErrorHandle.h"

static Error SYSTEM_NOT_INIT(-23,"system initailized fail,because some reason");

int foo()
{
  return SYSTEM_NOT_INIT;
}

int main()
{
    int err_code = foo();
    // print error details
    std::cout << Error::GetErrorString(err_code) << std::endl;
    return 0;
}

编程语言为何如此众多?


title: 编程语言为何如此众多
date: 2016-05-19 09:41:15
tags:
- 编程语言
- 编程范式

刚从大学毕业的人可能会说:学校老师说了,编程 = 算法 + 数据结构。多学习其它编程语言没意义,只需要学一种到精通就可以了。语言都是相通的,数据结构和算法才是灵魂。多年以后我才知道这句话的可笑。其实这话本身没有错,但是放在一些特定场景下是在回避问题,并没有正视。

前言

计算机软件领域为了解决一个语言不能解决的问题尝试发明了另一种语言,然后继续引入新的问题,然后继续发明语言,诸如CoffeeScript,TypeScript之于JavaScript,C#之于Java, Rust,Go之于C/C++。就这样在不断的制造问题和解决问题间徘徊,挣扎,反驳,批评和妥协使得语言的设计者和使用者为自己制造希望,同时又在希望的不确定性或是挫败感中找到前行的乐趣。

其实这样不同的创造语言本质是想解决一个语言带来的历史包袱,但是这样的包袱只要软件在不停的发展,那么它永远甩不掉,就像不可能把各种用C语言或Fortran写出来的基础软件设施推倒重新用拥有更先进的类型系统的Rust写一样,因为工程量巨大,推倒重写也不可能保证这些基础设施软件的正确性,耗资无法承受。想想世界上那么多科学计算库或底层的图形库,算法库,还有操作系统都是C/C++或者Fortran写的,这显然推倒重写是不可能完成的任务。想想Linux至今用了多少年,多少人力,写了多少行代码才发展到工业级别的操作系统?

上面说到的例子都是某语言想试图取代另一个语言,所以在大家看来,这些对立竞争的语言至少是平行的。但是还有一些语言的发明是试图用另一种思维角度来解决编程中遇到的问题,并没有形成竞争关系。所以就引出了编程范式(Programming Paradigm)一说。

编程范式

如今,编程语言百家争鸣,过程式编程语言,面向对象编程语言,函数式编程语言,甚至还有很多人接触很少听说的逻辑式编程语言,最后还有多范式融合的编程语言。过程式的经典语言是大名鼎鼎的C语言。面向对象的经典是Python,Java,C#。函数式的经典是LispHaskell。逻辑式语言的经典是Prolog(它能以另一种思维角度很方便的解决地图染色问题)。至于多范式融合的编程语言,C++可以算是,本来它的面向对象就不纯,至少与Java和Python相比是这样的。

当然,有心的人可能会发觉,现在很多主流的编程语言的发展演进都或多或少会借鉴其他范式的编程**啊。答案,是的。比如啊,Java里面的匿名类,Lambda表达式都是从函数式**里面继承过来的。现在的主流编程语言也慢慢从单一的范式过渡到多范式,这是好事,能向前看,不抱残守缺了:)

所以,面对这么多林林种种的语言,该如何学习?首先该学哪门语言?真像论坛上到处撕逼的那样:php是世界上最好的语言,Java才是王道,都是垃圾我Lisp语言才是各种语言的老祖宗?

那么,该怎么学嘛?我的认为是,取各个不同范式的经典语言学习,最后再选择一门主流编程语言学习做项目实战,这两步没有先后顺序,可以相辅相成。因为在我看来,至少会 OO, functional, imperative, logic 这四种 paradigm 的语言才叫“有基础”(这里的命令式编程语言就相当于冯诺依曼语言,也基本等同于通常所说的面向过程编程。),才有足够底气说“学一种语言就几个星期的事,语言都是相通的”。当然,如果不是跨范式的学习,确实能轻松不少,C++转Java和C#真的简单。

所以我从来不会笼统的说某个语言好或不好,而且建议多学习各种不同的编程范式,知道的越多,才能有可能对具体问题提出更好的解决方案。而且law of the instrument 中说:if all you have is a hammer, everything looks like a nail。另外有个观点大意是,语言(不管是人类还是编程)会限制思考。因为语言的设计和演化都被作者灌输了它自身的设计哲学(理念)。所以当你用某中语言来进行开发时,它(语言)本身就引导(改变)了你的思考方式。如果你不信我说的,去看看《七周七语言》,这本书是改变了我对编程语言和编程范式认知的第一本启蒙书:)

领域特定语言

领域特定语言(Domain-specific language,DSL)是在一些特定的专业领域里面处理专业问题而设计出来的语言,这些语言专门是为自身领域服务的,它们往往对自身领域的问题解决和表达更高效,比如Matlab,Mathematica这些软件的语言,Html/CSS也算一种网页的DSL。这种编程语言其实是最多的,编程语言有成千上万种,其中DSL占的比重很大,它们在各自的领域大放异彩,例如:专门处理气象数据和可视化的一种语言NCL等等。

参考

为什么现在会有这么多种编程语言

无题

  • 很多人都是对大脑运转原理理解不够,他们畏惧未知世界的力量,一直把眼光困在狭小而黑暗的世界里,等他们走出那个世界突然觉得也没什么。

  • 在黑暗中焦虑地摸索的年月,满怀着强烈的渴望,有过信心,也有过动摇和疲惫,但最后终于看见了光明。

manjaro下安装配置无线网卡驱动


title: manjaro下安装配置无线网卡驱动
date: 2018-12-15 23:13:47
tags:
- 无线网卡
- Linux

昨天买的小米新款笔记本到货,配置是i5 内存8G DDR4。反正是够我拿来写代码了 = =。 昨晚兴致勃勃地冲回新家,准备把小米本里面的原装windows 10 格了重做一个manjaro linux,但是奇怪的事情是,原本对硬件一向支持友好的manjaro居然装起来找不到无线网络,我靠居然没有支持,查了网卡的型号是Reatek的rtl8821ce的无线网卡,上网一搜,我擦,原来在去年的时候Linux还不支持这个型号的无线网卡。到今年为止才可以下载到这个型号网卡的驱动

而且一般OS都不内置,要自己安装,有些时候可能还得自己编译驱动。最奇怪的是,像manjaro这种内核滚动升级的,时刻保持最新的,居然内核没有带这个驱动。唉,暂时不去研究了。

说多了是泪。TT , 只有去小区背后的五金店买了个网线先插起来用着,然后通过:

yay -S rtl8821ce-dkms-git

来安装驱动,安装了驱动重启电脑,居然又没有起作用,后来一查, 才知道还需要安装内核的headers

无奈又通过:

sudo pacman -S linux419-headers

因为我是重装的manjaro所以是最新的manjaro18 内核是4.19.x,所以要安装对应的headers。

然后,我擦了,还不起作用,原来还需要启动驱动模块:

sudo modprobe 8821ce

然后manjaro的xfce的无线管理才能突然搜索到无线网络,唉,可真把我折腾的够呛。

家里面的网络布局方案


title: 家里面的网络布局方案
date: 2018-04-1 17:16:53
tags:
- 计算机网络

最近家里搞装修也是累,家里2层楼,至少得3个月。所以对于家庭里面的网络规划不能太忽视。

网络拓扑布局

大概就如下了。

入户光纤的第一台设备是光猫。接下来是路由器,再接下来是千兆交换机(可能是24口)

打算暂时只是把网线布局弄好,设备我以后自己可以慢慢买,所以目的是只用把网线接口布局弄好,网线接口预留好就可以了。还是选择用AP/AC的方案来做无线扩展覆盖。

最重要的要来了,因为以后想搭建家庭私有云,所以有个服务器机房(用夹层储物间)。这里是服务器等交换机设备的集中点,所以这个房间里网线接口和供电插座要多一点。根据文档附带平面图的红色长方形标记出来的网线接口来调整储物间网络中心的的网线接口数量。

因为家庭组千兆网络,所以用超六类网线(当然水晶头也是六类)。

网线接口平面标记图

以上平面图画四方形的就是网线预留的接口。
负一层的储物间用来做服务器机房。到时候会有机架式路由,交换机,还有电源接口。入户的光猫在2楼的书房的弱电箱中,然后网线接到负1楼的储物间连接路由器。

参考资料

https://www.zhihu.com/question/20479299

http://service.tp-link.com.cn/detail_article_458.html

最简单的计算机之有限自动机


title: 最简单的计算机之有限自动机
date: 2018-10-15 20:30:48
tags:
- 程序语言理论
- 计算理论

简单是终极的复杂 -- 达 芬奇

前言

其实这算是计算理论的第二个章节,第一个章节可以去看我之前写的文章----《程序的含义》

追求简单是人类内心的本性使然,看上去好像没有太多修饰,当你仔细揣摩,你会发现,最精华的理论已经融入每个角落,越是简单的东西,其实越难吧空,需要经过千锤百炼。

现代计算机具有强大的计算能力,但是正是由于其强大,所以伴随着过多的复杂性。我们很难理解一台计算机多个子系统的全部细节,更别说理解这些子系统如何互相协作从而构成整个系统了。这些复杂性使得对真实的计算机的能力与行为进行直接推导显得不切实际,此时计算机的简化模型就显得非常有用。简单的模型只提取出真实计算机中令人感兴趣的特性,它可以帮助人们建立对计算完整的认知。

接下来,我们会逐步揭开什么是计算,最后分析这样简单的模型所能达到的计算极限。

确定性有限自动机

现实中,计算机通常有大量的RAM和DISK,还有许多I/O设备(键盘等),还有CPU。有限状态机也被称作有限自动机,这是一个极简的计算机模型。它为了简单,抛弃了RAM DISK等这些特性。

状态 规则 输入 输出

有限状态机你可以把它看作是一个抽象机器,它拥有一些可能的状态,能跟踪到自己当前具体处于其中的一个状态。注意,它没有键盘等这些接口,它只有一个抽象接口,就是一个来自外部的信息输入流会被它逐个读取,并随着这些信息的输入,自动转移状态。

以下是一个有限状态机的图示:

至于状态机怎么看,我就不过多介绍了。如果学过《编译原理》的词法分析的章节,一定会接触到的。

  • 该机器有两个状态, 1和2
  • 该机器的输入字符集合为{a,b}
  • 该机器的初始状态是1

该机器会根据读到的字符来决定转移的状态,它不停的接受a,b在状态1和2之间来回切换,简直没完没了了。它像一个黑盒子一样运行,谁也不知道发生了啥,机器外面没人知道发生了什么,所以机器要有个输出,我们才知道最终的结果,有了结果,机器就停止运行了,相当于这个函数才有了输出,有输入必然要有输出,不然没意义。 所以我们需要为这台机器增加一个终止状态,以表示运行结束,产生输出。

我们暂且把状态2标记为终止状态,当然如果用在词法分析理论上也可以被称为接受状态,表明机器对某个序列是接受还是拒绝。修改好的状态机图示如下:

红色就表示终止状态。当然,正规点的自动机终止状态是画两个圆表示终止,我这里图个简便,请谅解下。

好了,我们来分析下。

上图的自动机初始化状态为1, 当读入一个字符a的时候,它会转移到状态2,这是一个终止状态,所以我们可以认为这自动机接受了字符串“a”。如果它又读取了一个字符b,状态又转移到了1,这不是终止状态,所以自动机不接受字符串“ab”,也就是拒绝了“ab”。所以很容易可以推理出来,自动机接受“a”,“aba”,“ababa”这样的字符串。如果把图示下面的字符b也换成a。那么自动机接受“a”,“aaa”,“aaaaa”这样奇数的a组成的字符串,拒绝“”,“aa”,“aaaa”这样偶数个字符a组成的字符串或空字符串。这台自动机就可以判断a组成的字符串是技术还是偶数,是足以称为最简单的计算机了。

当然,我们可以构造更复杂的自动机,我就不打算构造了。

确定性

显然,如果了解自动机概念的人,就知道之前所提到的自动机都是具有确定性,也就是说,无论它处于什么状态,并且无论读入什么字符,它最终所处的状态总是完全确定的。这样的确定性有2种约束条件:

  • 不存在二义性,也就是说一个状态对于同样的输入,它不能有多个规则

  • 每个状态都必须针对每个可能的输入字符至少有一个规则

具有这种确定性的自动机专业点叫确定性有限自动机(Deterministic Finite Automaton,DFA)。

用程序语言来实现DFA

DFA是一种抽象机器,也可以被认为是一种解释器,这种机器很容易用软件来模拟。

首先,需要定义一个规则集合RuleSet。

class FARule < Struct.new(:state, :character, :next_state)
  def applies_to?(state,character)
    self.state == state && self.character == character
  end

  def follow
    next_state;
  end

  def inspect
    "#<FARule #{state.inspect} --#{character}--> #{next_state}>"
  end
end

class DFARuleSet < Struct.new(:rules)
  def next_state(state,character)
    rule_for(state,character).follow
  end

  def rule_for(state,character)
    rules.detect { |rule| rule.applies_to?(state,character) } # find first if
  end
end

每个规则用一个FARule来表示,都有一个applies_to? 这样的API来判断某些输入情况下是否可以apply。DFARuleSet表示规则集合,相当于FARule类的一个容器,存放多个FARule。

好的,现在可以构造一个规则集合了:

=begin
#<struct DFARuleSet rules=[#<FARule 1 --a--> 2>, #<FARule 1 --b--> 1>, #<FARule 2 --a--> 2>, 
#<FARule 2 --b--> 3>, #<FARule 3 --a--> 3>, #<FARule 3 --b--> 3>]>
=end
rules = DFARuleSet.new([
  FARule.new(1,'a',2), FARule.new(1,'b',1),
  FARule.new(2,'a',2), FARule.new(2,'b',3),
  FARule.new(3,'a',3), FARule.new(3,'b',3)
])

# 测试DFARuleSet类

# => 2
rules.next_state(1,'a')
# => 1
rules.next_state(1,'b')
# => 3
rules.next_state(2,'b')

到这里,是否能根据这个规则集合画出对应的DFA的图呢,这是可以的。下图就是以上RuleSet对应的自动机

DFA

不过,这台自动机没有终止状态,还不完整,所以我们要编写一个DFA的类的表示DFA来配置RuleSet和初始状态,终止状态,加入读取字符流的接口,并模拟抽象机器的运行,这样就灵活了。计算机科学很重要的一点就是抽象思维,分离变化与不变化。是不是跟上一章讲解语义的一样呢?其实都相当于一台解释器。

class DFA < Struct.new(:current_state,:final_states,:ruleset)
  def accepting?
    final_states.include?(current_state)
  end

  def read_char(character)
    self.current_state = ruleset.next_state(current_state,character)
  end

  def read_string(str)
    str.chars.each do |character|
      read_char(character)
    end
  end  
end

# 测试下DFA

# => true
DFA.new(1,[1,3],rules).accepting?

# => false
DFA.new(1,[3],rules).accepting?

之后,为了更加方便,我们构造一个可以创建DFA的工厂类:

class DFAMaker < Struct.new(:start_state, :final_states, :ruleset)
  def make_dfa
    DFA.new(start_state,final_states,ruleset)
  end
  
  def accepts?(str)
    make_dfa.tap { |dfa| dfa.read_string(str) }.accepting?
  end
end

# => false
DFAMaker.new(1,[3],rules).accepts?('a')

# => false
DFAMaker.new(1,[3],rules).accepts?('baa')

# => true
DFAMaker.new(1,[3],rules).accepts?('babab')

# => true
DFAMaker.new(1,[2],rules).accepts?('aaaa')

# => false
DFAMaker.new(1,[1],rules).accepts?('a')

# => true
DFAMaker.new(1,[1],rules).accepts?('b')

非确定性有限自动机

之前的确定性有限自动机(DFA)理解和实现起来都很简单,那是因为DFA与我们熟悉的机器非常相似。

在去除了一台真实的计算机的所有复杂性之后,我们有机会使用一些不太常见的**去进行探索,这将让我们离本质会更进一步。

一种探索方式是去除我们现在所有的假设和约束。首先,确定性约束似乎是个限制,因为可能我们有些时候并不关系每个状态上每个可能的输入,那么为什么不能忽略不关心的字符处理规则,并假设当异常发生时这台机器能进入到一个通用的失败状态呢? 那好,我们再进一步放宽要求,如果允许这台自动机拥有对立的规则,那么就会导致机器有多个执行路径,这又意味着什么?

现在我们将要探索这些想法,需要对DFA的能力稍微做调整一下,看看有没有新的可能。

非确定性

假设我们想要一台自动机,它能接受由a和b组成的第三个字符是b的任意字符串,那么很容易想到以下DFA的图示:

DFA图示

以上的DFA处于状态3的时候,一旦读取到b就终止了,如果后面还有输入字符,就不停的循环进入终止状态5,反正目的达到了,只要是第三个字符是b的任意字符串就可以了,但是状态3读到了a字符,那么之后无论遇到什么输入,直到字符流读取完成,字符串也不能被该DFA识别了。

到这里你发现DFA好强大呀,好像没什么不能完成的任务(对于特定字符串识别)。但是你错了,现在我提出一个问题,请设计一台自动机,满足识别倒数第三个字符是b的任意字符串,请问该怎么设计?(这里先别往下看,留一段时间在纸上画画图)。

经过一段时间的思考,有人会发现这似乎会很困难,因为上面的DFA读取正数第三个的字符是b的时候保证在状态3,但是如果是倒数第三个就困难了,自动机很难预先知道什么时候能读到倒数第三,因为在整个字符流读取完成之前它不知道这个字符串有多长。现在我来告诉你,几乎不存在一台DFA能识别这样的需求的字符串。

这时候要加强DFA的能力就需要放宽要求,把它改了,把确定性改成非确定性,换一个概念得了,叫非确定性有限自动机(NFA)。

NFA就是对于每个状态特定的输入结果下,跳转的下一个状态都是不确定的。这个不确定的概念给自动机带来了更加强大的能力。这样就可以很轻松设计以下一台NFA:

NFA图示

查看以上NFA,对于状态1来说,接受一个字符b,它有可能保持在状态1,也可能跳转到状态2。也就是,对于状态1来说,字符b的输入导致的结果是不确定的。

用以上NFA来识别字符串“baa”,读取完字符串之后,最终状态可能停留在状态1,也可能停留在状态4接受态。所以用NFA来识别字符串就要遍历所有可能的状态执行路径,只要存在一种路径能达到终止态,那么该字符串就被这台NFA所接受识别。哈,这么来看有点暴力搜索了,但是没办法,因为我们现实世界的物理计算机就是确定性的计算机,要模拟非确定性只有这样的办法了

这样的暴力方式很容易可以看到两种实现,一个是采用递归,一种是采用线程并行,但是这两种实现都有点复杂和低效。

我们可以采用一种更简洁高效的模拟,模仿DFA的模拟实现。

很简单,最开始与DFA类似,也要创建一个RuleSet来存放规则集合。它与DFA不一样的是,在某个状态时接收相同的输入,可能跳转转移的状态不一致,也就是可能下一个状态是多个可能,所以需要引入编程语言提供的集合(Set)的概念来给下一个状态集去重。

require 'set'

class FARule < Struct.new(:state, :character, :next_state)
  def applies_to?(state,character)
    self.state == state && self.character == character
  end

  def follow
    next_state;
  end

  def inspect
    "#<FARule #{state.inspect} --#{character}--> #{next_state}>"
  end
end

class NFARuleSet < Struct.new(:rules)
  def next_states(states, character)
    states.flat_map { |state| follow_rules_for(state, character) }.to_set
  end

  def follow_rules_for(state, character)
    rules_for(state, character).map(&:follow)
  end

  def rules_for(state, character)
    rules.select { |rule| rule.applies_to?(state, character) } # select * where 
  end
end

好的,现在我们来用以上的Ruby类来定义上一副图示所表示的NFA:

ruleset = NFARuleSet.new([
  FARule.new(1, 'a', 1), FARule.new(1, 'b', 1), FARule.new(1, 'b', 2),
  FARule.new(2, 'a', 3), FARule.new(2, 'b', 3), FARule.new(3, 'a', 4),
  FARule.new(3, 'b', 4)
])

# => #<Set: {1, 2}>
ruleset.next_states(Set[1], 'b')
# => #<Set: {1, 3, 4}>
ruleset.next_states(Set[1,2,3], 'a')
# => #<Set: {1, 2, 4}>
ruleset.next_states(Set[1,3], 'b')

对ruleset的定义能看得出来吗? next_states的输入参数是当前状态集合,第二个参数是当前状态的输入数据,返回值是接收输入数据后返回的下一个状态集合。

好的,再下一步就是通过程序来模拟手工的NFA运行,与DFA本质上并没有区别:

class NFA < Struct.new(:current_states, :final_states, :ruleset)
  def accepting?
    (current_states & final_states).any?       # set intersection operation is empty?
  end

  def read_char(character)
    self.current_states = ruleset.next_states(current_states,character)
  end

  def read_string(str)
    str.chars.each do |character| 
      read_char(character)
    end
  end
end

class NFAMaker < Struct.new(:start_state, :final_states, :ruleset)
  def make_nfa
    NFA.new(Set[start_state], final_states, ruleset)
  end
  
  def accepts?(str)
    make_nfa.tap { |nfa| nfa.read_string(str) }.accepting?
  end
end

nfa = NFAMaker.new(1, [4], ruleset)
# => true
nfa.accepts?('bab')
# => true
nfa.accepts?('bababab')
# => false
nfa.accepts?('babababa')
# => true
nfa.accepts?('bababababbbbbb')

哈哈,以上代码最终实现的效果与NFA的的图示所达到的效果一样了,能接受"bab",“baa”这样倒数第三个字符是'b'字符的字符串。

自由移动

上一小节中,我们看到了对确定性这个约束条件的放宽给设计NFA的方法带来了一定的表达能力,为了再次提高表达,是否还可以放松哪些条件呢?

回到DFA的层面,很容易设计一台DFA,这个DFA能接受字符a组成的字符串长度是2的倍数(“aa”, "aaaa", ......)。

但是,如何设计一台机器,让这台机器既能接受长度是2的倍数也能接受是3的倍数的字符串呢? 由于之前了解过NFA的概念,于是有人可能随手就实现了以下一台NFA:

以上这台NFA确实能接受"aa" "aaaa" "aaa" "aaaaaa"这样的字符串,满足要求。但是仔细一看,其实它也能接受"aaaaa" 这样不满足设计要求的字符串。我靠,不符合设计啊。如果不经过严密思考,一旦NFA比较复杂,那么这种错误一般比较难发现。怎么办呢?

其实已经很接近真相了,最主要是因为这一台NFA要满足2个条件,这两个条件写在一起了,其实我们可以把这两个条件,根据每个条件写出对应的DFA,然后把DFA组合在一起形成一个规模大点的NFA机器。

我们把这种组合特性叫做自由移动,这个特性就是能让NFA在多组状态中做选择的。我们把自由移动这种特性用没有转移输入的箭头来表示:

上面这台NFA,但如果在当前是状态1。那么它的当前状态就既是2也是4的状态。也就是说,这台NFA的初始状态集合是[1,2,4]。状态1流转只能到2或4,这样就分叉了,根据箭头是回不去状态1的,这样2台DFA各司其职。

那么如果用Ruby怎么模拟NFA中的自由移动呢? 其实就在RuleSet的规则集合中定义一个无输入并且自由流转的规则。 好的,我们根据这点来根据上图所示的NFA,设置它的规则:

ruleset = NFARuleSet.new([
  FARule.new(1, nil, 2), FARule.new(1, nil, 4), 
  FARule.new(2, 'a', 3),
  FARule.new(3, 'a', 2), 
  FARule.new(4, 'a', 5), 
  FARule.new(5, 'a', 6),
  FARule.new(6, 'a', 4)
])

# => #<Set: {2, 4}>
ruleset.next_states(Set[1], nil)

状态1在无输入的情况下的下一个状态集合就是[2,4]。然后需要在RulSet的类中添加一个方法来计算一个状态集合的自由移动,这个方法同样返回一个状态集合:

class NFARuleSet
  def follow_free_moves(states)
    more_states = next_states(states, nil)

    if more_states.subset?(states)
      states
    else
      follow_free_moves(states + more_states)
    end
  end
end

# =>  #<Set: {1, 2, 4}>
ruleset.follow_free_moves(Set[1])

然后,要模拟当前状态的自由移动,重写NFA类中的current_states:

class NFA
  def current_states
    ruleset.follow_free_moves(super)
  end
end

这样的意图是,确保NFA当前可能的状态总是通过自由移动流转到任何可能的状态。这就让访问当前状态透明了,之前的代码几乎不用改动。好了,现在可以用之前的NFAMaker构造出一个支持自由移动的NFA了,并且按照上图的NFA设计图,并确保代码可以正确工作了:

nfa = NFAMaker.new(1, [2, 4], ruleset)
# => true
nfa.accepts?('aa')
# => true
nfa.accepts?('aaa')
# => false
nfa.accepts?('aaaaa')
# => true
nfa.accepts?('aaaaaa')

看运行结果,恩,与设想中的一样。能接受2或3的倍数,并且不会出错了。

模拟自由移动比现象中简单,它在非确定的基础上给了设计者额外的设计自由。当然从自动机理论的角度看,这文章中的很多名词并不专业,有限自动机读取的字符一般叫符号(Symbol),状态之间移动的规则叫做转移(transition),组成一台机器的规则集合叫做转移函数,表示空字符串的数学符号是希腊字母ε,能自由移动的NFA称为NFA-ε,自由移动一般叫ε转移。

如果你学过编译原理的国外教材,在讲状态机的时候就会接触到这些,比如把正则表达式通过NFA写出来,就会用到ε转移来组合各种状态机,然后通过子集构造法把NFA规约成DFA。 后面我会讲到有限状态机与正则表达式是等价的,相当与正则表达式是有限自动机的文本表示出来的语言(正则语言,有时候也叫正规语言),而有限状态机是正则语言的解释器。所以当你想从底层实现正则表达式引擎的话,无论如何也绕不过有限状态机理论的。当然有些高级做法,并没有把正则表达式翻译成自动机执行,但是我这里不想过多涉及这样工业界的高级解释器技巧,这已经偏离文章宗旨。

正则语言

正则语言也叫正则表达式,这是一个很简单的语言,正因为太简单了,所以在表达一些复杂模式匹配的时候语言会有些晦涩。但是它的单个规则很简单,都是通过简单的规则组合起来用的。这个子章节,我需要用Ruby构造的NFA一步一步来构建最简单的正则表达式的解释器。

(To be contined)

零知识证明综述


title: 零知识证明综述
date: 2019-08-20 10:14:16
tags:
- 密码学

前言

零知识证明看样子是很复杂的一个过程,其实不是,网络上对这个概念的讲解杂七杂八,质量参差不齐,今天我就打算讲一讲有关话题,我会循序渐进,每个小节我会逐渐加深理解难度,然后最后导出工业界的做法。所以读者最终要的是,首先要做到不要畏惧。实在想不明白可以用手稿纸画一画,推一推。我写文章通常的做法会在专业的名词用括号注释英文,目的是让读者不混淆,并且英文搜索到的资料更准确。

研究背景

传统的通信协议中的一个共同弱点就是容易遭遇到信道的窃听,所以零知识证明(又叫零知识协议Zero Knownledge protocol,ZKP)就这样诞生了。目的是构造这样一个系统,该系统中的证明人(Prover)可以在不暴露任何信息(Zero Knowledge)的情况下,让验证者(Verifer)进行验证。简而言之,对于监听者,就是你监听了,也没用,你监听到的信息也推导不出来任何有用的东西。

简介

这里要注意下,虽说到这里反复提到了证明(Proof)这个词,但是这个证明不是数学上的证明,数学证明是严格的,使用自证陈述或从预先建立好的证明中获得陈述。ZKP更类似于人类在信息交换过程中建立一个陈述的动态过程,所以它是互交式的(当然,也有非互交式的,这个到后面会提到,而工业界的zk-SNARK用的就是非互交式的)。

简而言之,在ZKP中,所谓互交式就是证明者(Prover)不为它持有的秘密(Secret)直接提供证据,而是双方在一个互交来回“对话”的过程中证明者(Prover)逐渐的说服验证者(Verfier)他持有某个秘密是真实的。Prover也不用暴露秘密就可以说服对方,这样太好了。比如,我可以宣称对外界粉丝说,刘亦菲是我老婆,也不用把结婚证(Secret)拿出来,就能向粉丝们证明我说的是真的(刘亦菲是我老婆),即真命题。哈哈,结婚证是我的秘密隐私啊,怎么可能拿出来给粉丝看。

ZKP中的参与者

在ZKP中,参与者也就是2方:

  • Alice (Prover) Alice想向Bob传达某种知识的证明,但是同时又不想暴露她的秘密

  • Bob (Verifier) Bob向Alice提出一些问题,这有助于让Bob确认Alice是否知道她声称知道的某些东西, Bob无法从这些互动中学到有用的任何东西,即使Bob在欺骗或从事ZKP以外的活动。

一个简单的例子

为了举例,考虑一个由圆形隧道组成的洞穴。与这个洞穴的入口正好相反,有一扇门只能通过密码打开。尽管这种情况可能不是真实的场景,但这个场景演示基本涵盖了ZKP的基本特性,现在Alice知道了这扇门的密码,她想向Bob证明这一点,但又不想把门的密码暴露出来。他们两个人要互动地完成以下任务:

  • Alice(绿脸)随机选择一个洞穴的分支进去(左或者右都行),这个不能让Bob(黑脸)知道她选择了哪一个分支

  • 站在洞穴入口处的Bob,他随机选择一个分支,然后让Alice从他选择的那个分支出来

  • 如果Alice真的知道门(红色的线条)的密码,没有撒谎的话,她就可以每次做到从Bob选择的分支出来。如果她不知道门的密码,那她在最开始就有百分之50的概率蒙骗过关,随着这样次数的逐渐增加,Alice蒙骗过关的概率越来越低,在达到一个特定的次数的时候,她蒙骗过关的概率可以小到忽略不计了,比中彩票的概率还要小。

这一个例子也展示了ZKP的另一个特性,Bob确信了Alice有门的密码,但是Bob不能使别人相信Alice有门的密码,这个过程只有他两知道,把过程录下来也无济于事,因为录像带可以造假。所有没有任何有用的信息可以暴露给Bob,更不可能流向协议系统之外,只有Alice自己知道门的密码。

ZKP的特性

所以ZKP的特性可以总结如下:

  • 验证者(Verifier)无法从协议中学习到任何有用的知识。这是ZKP的核心特性,也就是说零数量的知识被暴露出来。当然有个类似的协议叫最小披露协议(Minimum Disclosure Protocol),它适当放宽了这个特性要求,试图暴露一个尽可能最小的知识出去。

  • 证明者(Prover)无法欺骗验证者(Verifier)。随着协议互交轮次的数量提高,Prover欺骗Verifier的概率急剧降低,慢慢小到可以忽略不计。

  • 验证者(Verifier)也无法欺骗证明者(Prover)。即使验证者不遵守协议规则,验证者也无法从协议中获取任何信息,验证者只能慢慢确信证明者陈述的某个命题是真的。

  • 验证者(Verifier)无法向外界证明证明者(Prover)所说的是真的。比如,我(Prover)通过ZKP向某个刘亦菲的粉丝(Verifier)证明了刘亦菲是我老婆,但是这个粉丝(Verifier)不能向其他粉丝证明刘亦菲是我老婆。只有天知地知,你知我知。

好的,接下来在引出ZKP的定义之前,我们需要讨论一些诸如此类证明系统一些必要属性,这些属性包括正确性和完备性。

互交式证明系统

零知识协议是交互式证明系统的实例之一,其中证明者和验证者互相交换挑战,然后响应,通常依赖于允许他们保密的随机数(理想情况下,公平投币的结果)。正如我们前面所说,在这种情况下,证明是概率的,而不是数学意义上那种绝对的。这些证明只需要在一定的有界概率下才是正确的(尽管这个概率可以任意接近100%)。交互证明有时被称为协议证明。

用于识别的交互证明可以被表述为知识的证明。Prover有一个秘密(记为s),他希望通过正确地回答出需要秘密s作为依据推导而出的知识才能回答Verifier的提问,使Verifier相信Prover他知道s。值得注意的是,证明s的知识与证明s的存在性是完全不同的,例如,证明某个x是模n的二次剩余与证明x的平方根模n的知识是不同的,表示出来就是 k^2 = x mod n 给定x和n,如果k在该二次同余方程下有解,那么则得出 k = square(x) mod n。(实在不明白搜二次剩余,数论的东西)

如果一个交互式的证明具有完备性和可靠性,那么它就是知识证明。

  • 完备性的定义(Completeness Property)

如果给定一个诚实的Prover和一个诚实的Verifier,协议以几乎100%的概率成功(即,Verifier接受Prover的声明),则交互式证明协议是完备的。当然,几乎100%的定义取决于应用,但通常意味着失败的概率在这个场景下并不具有实际意义。(也就是低到可以忽略了)

当然,还可以用形式化点的方式来描述,设Prover和Verifier是一对互交的概率图灵机,随机变量<P(i),V(j)>(x)表示图灵机Verifier与Prover完成互交问答后的输出(这个输出是一个概率值),<>其中x为它们的公共输入,所以可以化简为<P(i),V(j)>(x) = 1表示V接受P给出的证明,<P(i),V(j)>(x) = 0表示V拒绝P给出的证明。其中i,j都是P和V各自的随机输入变量(均匀独立的)。<P,V>这对互交图灵机表示语言L的互交证明系统。

Probability(<P(i),V(j)>(x) = 1) > 1 - c(|x|) 其中函数c表示完备性错误概率,可忽略。 x属于语言L

以上式子表示完备性成功的概率极大。

  • 可靠性的定义(Soundness Property)

如果存在具有以下性质的期望多项式时间算法m,则交互式证明协议是可靠的:如果不诚实的Prover能够以不可忽略的概率与Verfier成功执行协议(成功概率极大,反过来就是说失败率极小),则m可用于从该Prover中提取知识(本质上等同于Prover的秘密)以几乎100%的概率允许随后(下一轮,依次递推)的协议执行

用形式化的方式来说就是:

B代表任意的Prover,也就是可以认为是不诚实的Prover

Probability(<B(i),V(j)>(x) = 1) < s(|x|) 其中函数s为可靠性错误概率,可忽略。x不属于语言L

以上式子表示可靠性失败的概率极小。

所以总而言之,可以这么说,对于任意的Verifier,存在一个多项式时间的算法m(x)(通常成为模拟器),使得Verifier在互交过程中得到的所有信息都可以直接利用算法m(x)模拟出来,也就是说Verifier从Prover那里得到的所有信息都可以用算法m计算得到。简而言之就是,Verifier没有从Prover那里获得任何额外的信息。

因为想冒充Prover就必须拥有这个secret,所以协议的可靠性就是靠提供与secret的等价的知识证明来保证的,这个属性条件保证了不诚实(作恶,假冒)的Prover成功。所以又可以反向得出一个方法,就是,证明某个协议是正确的标准方法就是是假设有一个不诚实(作恶,假冒)的Prover能够成功地执行该协议,并说明他是如何在多项式时间内计算出secret的。

这种“知识证明”**是零知识证明的基础。然而,很明显,这两个属性都没有提到零知识本身。此外,ZKP还应该具有这样的属性,即在Prover和Verifier之间不应该传递Verifier没有Prover的帮助来识别的知识量。换句话说就是,Prover和Verifier之间传播的知识都是经过Prover认同的。此属性简单地称为零知识属性,将在引入模拟器的概念后定义,如下所述。

模拟器

让我们再考虑一下之前Alice和Bob洞穴例子。假设Bob给他的朋友Jack发了一盘录像带,上面是他和Alice一起证明所采取的一系列步骤。我们将这种磁带称为证据的一个视图(或记录本)。Jack指Bob伯伪造录音带,很明显Bob不能做任何事情来说服Jack。Bob没有任何不可伪造的、不可否认的证据,来证明Alice确实知道门的密码。Bob所能做的就是让Alice再次为Jack演示一遍,Jack将为Alice挑选她出来的分支序列,比如左右左右左(这保证了随机性)。如果有一种方法可以伪造一个与真实的证据不可区分的证据(比如录像带),我们就说有一个模拟器可以模拟所讨论的证据。

相当于模拟器就是那个录像机,录像机就是那个多项式算法m。多项式算法m生成的结果就是那个录像带,录像带就是视图。

  • 模拟器(simulator)

模拟器是一种方法或算法过程(多项式算法m,),它生成的假(生成不需要Prover参与)视图与证明的真(生成时需要Prover参与)视图是不可区分的。也就是说,无论录像带真假,反正Jack是看不出来真假的,只能一股脑认为是假的,没办法,只有他自己与Alice跑协议去,自己去验证。

所以模拟器这个概念是上述零知识属性定义的关键。

  • 零知识属性(zero knowledge property)

如果存在证明的模拟器,则知识证明具有零知识属性。

这使得文章前面几节所描述的内容都用上了,意思是说,在zkp的上下文中,Verifier除了获得secret的有效性之外,不会获得有关该secret的更多信息。此外,这类协议的一个非常理想的特性是,Prover参与协议的次数不会改变模拟攻击成功的机会。这样零知识属性使我们可以得到如下零知识证明的定义。

  • 零知识证明

零知识证明是同时具有零知识属性的知识证明。

零知识证明的基本定义外延

还有一些基本定义的外延是比较重要的,需要了解。

说如果一个交互式证明是零知识的,那么存在一个simulator(算法m),使得Verifier与真正的Prover交互产生的证明副本(录像带)的整体分布(ensemble distribution)和与一个simulator交互产生的证明副本(录像带)的整体分布等价。

  • 完全零知识(Perfect Zero knowledge)

如果真实和模拟的记录本(录像带)彼此之间完全不可区分,则协议称为完全零知识。即两个分布完全等价,这个定义是好理解的,但却是最苛刻的。

  • 统计零知识(Statistical Zero Knowledge)

如果真实和模拟的记录本(录像带)的概率分布之间存在可忽略的差异,则协议是统计零知识。两个分布是统计闭合的(statistical close)。统计闭这个名词,它是说两个分布本身相差很小很小(可忽略)

  • 计算零知识(Computational Zero Knowledge)

如果受限于概率多项式时间测试的观察者不能区分真实和模拟的记录本(录像带),则协议的计算零知识。即两个分布多项式不可区分。

所以, 完全零知识强于 > 统计零知识强于 > 计算零知识

显然,计算零知识是对基本概念的采取的是一个最宽松的限制。尽管如此,它仍然是非常有用的,因为在实践中,“攻击者”通常被认为拥有对我们的系统进行攻击的多项式时间能力。已经定义了其他外延,例如非交互式零知识协议,其中Prover不必在场以使Verifier相信其知识。本文将不讨论这些外延。

还有其他类型的证明虽然不是零知识的,但是也不是没用到零知识和最小披露协议。这些证明中与零知识证明的区别变化仅仅是在协议执行过程中,允许从Prover流向Verifier的信息量和信息类型在可控或很小的范围内。

对互交式证明系统的总结评论

很明显,零知识性和可靠性在系统所呈现的安全级别上是不稳定的。对于一个给定的协议来说,依赖于计算困难的问题对其安全性至关重要。对于最见的问题(如大整数分解、背包问题、离散对数等)没有证明,因此使用它们的系统的安全性直接取决于计算复杂性领域的未来发展。这种类型的系统通常被称为可证明安全的。

在零知识技术(ZKP)和公钥(PKP)技术之间的差异可以得到一些优劣比较,这些是:

  • 使用时没有降级:重复使用ZKP不会出现降级。ZKP也能抵抗选定的文本攻击。这导致了一个不可证明安全的ZKP被认为是取代可证明安全的Public Key Protocol的。

  • 效率:ZKP通常比PKP效率低。在某些需要确保(硬或软)实时计算的应用环境中,这是需要考虑的一个重要因素

  • 未经验证的假设:大多数ZKP和PKP依赖于相同的假设(二次剩余、因子分解、离散对数等)

NP ∈ ZKP

本节简要介绍了计算复杂性类的P问题和NP问题。这对于理解每一个NP问题都有与之相关的零知识证明这一结论是很有必要性的。这一结论的证明将在本节末尾通过证明NP完全问题(NPC)的解的知识的简单协议来概述。简单来说就是,每一个NP问题都可以用来构造零知识证明协议。

NPC问题介绍

还是先来回顾下P问题,NP问题,NP-Complete问题,NP-hard问题这四个概念的关系吧。

如下图,每个概念会逐一讲解:

首先,P问题中的P是指多项式时间复杂度(Polynomial time)。时间复杂度并不是表示一个程序解决问题需要花多少时间,而是当程序所处理的问题规模扩大后,程序需要的时间长度对应增长得有多快。也就是说,对于某一个程序,其处理某一个特定数据的效率不能衡量该程序的好坏,而应该看当这个数据的规模变大到数百倍后,程序运行时间是否还是一样,或者也跟着慢了数百倍,或者变慢了数万倍。

多项式就不用解释了,学习过微积分的都会讲这个概念,像O(1),O(ln(n)),O(n^a)等,我们把它叫做多项式级复杂度,因为它的规模n出现在底数的位置;另一种像是O(a^n)和O(n!)等,它是非多项式级的复杂度,其复杂度计算机往往不能承受。当我们在解决一个问题时,我们选择的算法通常都需要是多项式级的复杂度,非多项式级的复杂度需要的时间太多,往往会超时,除非是数据规模非常非常小(没有实用性)。

  • P问题,能在多项式时间内找到解的问题。一般我们在LeetCode上刷的题通常都是P问题,都能在多项式时间内找到正解。所有P问题都是NP问题,能多项式地解决一个问题,必然能多项式地验证一个问题的解。

  • NP问题,在多项式时间内“可验证”的问题。也就是说,不能判定这个问题到底有没有解,而是猜出一个解来在多项式时间内证明这个解是否正确。即该问题的猜测过程是不确定的,而对其某一个解的验证则能够在多项式时间内完成。P类问题属于NP问题,但NP类问题不一定属于P类问题。比如,哈密顿回路(Hamilton Cycle)就是NP问题,当然,它也是NPC问题。

  • NPC问题,NP问题中的特殊存在,所有的NP问题都可以归约(归约就是做一次或多次多项式时间的变换)到它。没有人能够找出求解NPC问题的多项式时间的算法,同时也没有人能够证明对于这类问题不存在多项式时间算法。正是由于NPC的存在,才使大多数人相信 P != NP。

  1. NPC问题首先要是一个NP问题。
  2. 其次,所有NP问题都能归约到它

之前说到归约,比如问题A能归约到问题B,那么用问题B的解法,一定能解决问题A,虽然解决问题B的时间复杂度 >= 解决A的时间复杂度。这样不停的从小的NP问题归约上去,最终会归约到NPC问题,相当与树结构的根节点(终极存在)。所以,NPC问题的时间复杂度 >= NP问题的复杂度。NPC问题是最难的。

比如,哈密顿回路也可以归约到旅行商问题(Travelling Salesman Problem),所以旅行商问题也被证明是NPC问题了。所以,NPC问题只有暴力搜索了,一般是图论相关的问题,动不动就会搞出这么个事儿来。所以涉及到图相关的问题一般比较复杂。

  • NP-Hard问题,NP-Hard问题是这样一种问题,它满足NPC问题定义的第二条但不一定要满足第一条。

NP-Hard问题同样难以找到多项式的算法,但它不列入我们的研究范围,因为它不一定是NP问题。即使NPC问题发现了多项式级的算法,NP-Hard问题有可能仍然无法得到多项式级的算法。NPC问题一定是NP-Hard的问题。NP-hard一般是属于不在研究范畴内,但是它可能比NPC问题更难,它放宽了限制。比如,这篇论文的证明,星际争霸可能是NP-Hard的。具体地说,给定一个初始布局(包括地图、双方已有资源、双方已有建筑、双方已有兵力),判断其中一方是否能获胜,这个问题是 NP-hard 的。

如果存在非均匀的多项式加密方案,那么每个NP语言都有一个互交式的计算零知识证明系统。因为NPC语言都有一个零知识证明系统,比如地图3着色(G3C)。因为G3C是个NPC问题,所以任何NP问题都可以归约到G3C问题,最后得出,所有的NP问题,都有一个与之关联的零知识证明系统。

G3C ∈ ZKP

注,下面提到的“图”是指数据结构所说的图,边也是图的边(edge)。

G3C所谓的着色也是给不同的图的节点(node)着色,不是给边(edge)着色,这点要明确。

假设Prover希望说服一个Verifier知道他对某一图(graph)是用三种不同的颜色着色的,而且不能直接完全暴露出整个图的着色状态。Prover可以按照|E^n|个阶段(其中E是图的边的个数)的顺序执行操作,每个阶段包含以下步骤:

  • 1.Prover随意(随机)排列三种颜色,这使得Prover能够在重复这些步骤的过程中隐藏真实的色彩。
  • 2.Prover的着色色彩对Verifier是不可见的,是隐藏的。(需要用到一些加密手段)
  • 3.Verifier随意(随机)选择图(graph)的一条边(edge)。
  • 4.Prover把选中的边连接的两个节点的颜色暴露出来给Verifier查看。
  • 5.Verifier确定两个节点的颜色是否合法,比如节点颜色是否不同。
  • 重复步骤1

其中,|E^n|中的n是,进行所有步骤的轮次,当n进行的足够多的时候,Verifier确信Prover所说的是真命题的概率会越来越大。概率是 1 - (1 / E)^n。当跟足够大时, 概率就很接近100%了。

反过来说,(1 / E)^n就是Prover欺骗Verifier的概率随着n的变大会逐渐变小,哪有这么好的运气每次都欺骗成功?如果地图不是三种颜色的,而是四种,那么每一轮Prover欺骗Verifier的概率就是(1 / E)。

如果还看不明白以上步骤,那么MIT某个网页上有地图三着色的Demo演示,可以鼠标操作,你扮演一个Verifier,这样会让你理解更直观些。如果网页挂了,请搜索Interactive zero knowledge 3-colorability demonstration。

从上面的步骤可以看出来,G3C它是计算零知识证明系统。这也再次肯定了,每个NP语言(问题)都有与之相关的一个计算零知识证明系统。

应用

接下来,我将介绍真正的零知识系统的工作机制,也就是业界做法,ZKP的一个主要应用之一就是获得一个认证的保密目标(显而易见,这就是ZKP最直观的理解)。ZKP系统可以很好地解决金融或其他安全关键应用程序中的安全问题,在这些应用程序中,比如,智能卡等系统不够安全。智能卡容易受逆向工程的影响,被“破解”。如果利用ZKP,逆向工程也无法提取重要信息。可以实现零知识证明系统来抵御此类攻击。此外,ZKP系统可以在不同程度上来适配应用。

以下我会用两个例子说明问题,第一个是关于ZKP做认证的(身份认证),第二个是用来证明(ZKP上下文中的证明,非数学证明)图同构(graph isomorphisms)的系统。

Feige-Fiat-Shamir(FFS)身份鉴别算法

这个算法是最著名的身份的零知识证明,是由Feige,Fiat,Shamir这三个人从他们发明的鉴别和数字签名的方案改进过来的。1986年7月9号,这三个设计者递交了一份美国专利申请。但是由于其算法有军事上潜在的应用,由专利改成了保密的密令,就是此成果将严格保密,否则判两年监禁,罚款2W。但是设计者不是美国公民,这种要求太不可思议了,而且所有的工作都是在以色列进行的,跟美国毫无关系。这消息传遍了整个学术界和出版界。两天内,密令被取消,然而没有得到任何官方解释。由此可体现,这个算法刚出来就那么牛逼。这些题外故事有兴趣可以看wiki。当然这个算法是互交式的,不是非互交的。

和之前的例子差不多,这里的目标是让Alice通过展示她对某个秘密s的了解,来向Bob证明她的身份。通过这些授权的公共数据,这些秘密s是与Alice相关联的。FFS系统的安全性取决于提取未知因子分解的平方根模大复合整数的假设,这个问题是困难的。如果这个假设的困难没了,那么也就不安全了。

身份的Feige-Fiat-Shamir零知识证明

  • 预先计算(setup阶段): 在发放私钥之前,仲裁者(一个可信,独立的实体)随机选取一个模数n,n为两个大素数的乘积。实际上,n是512到1024 bit的数值。n必须至少为512bit,并尽可能接近1024 bit。为了生成Alice的公钥和私钥对,可信仲裁者选取一个数v,v对模n的二次剩余。也就是说,x^2 ≡ v (mod n)有一个解,且v^-1 mod n存在 。v就是Alice的公钥,然后计算,s ≡ sqrt(v^-1) (mod n)的最小s,将它作为Alice的私钥。

这样,该零知识证明的协议就正式开始了(n和v公开):

  • Alice选取一个随机数r,r < n, 接着计算x = r^2 mod n,并将x发送给Bob。

  • Bob发送一个随机位b给Alice。(注意是一个随机位,bit)

  • 如果b = 0, Alice将y = r * s^b mod n,也就是y = r mod n发给Bob; 如果b = 1, Alice计算y = r * s^b mod n,并将y = r * s mod n发送给Bob。

  • 如果b = 0, Bob验证y^2≡x*v^b (mod n), 可以简化为y^2 ≡ x (mod n), 因为y ≡ r (mod n), 所以 r^2 ≡ x (mod n), x ≡ r^2 (mod n), 以证实Alice知道sqrt(x) ≡ r (mod n); 如果b = 1. Bob验证y^2 ≡ x * v^b (mod n), 化简为y^2 ≡ x * v (mod n), 因为y ≡ r * s (mod n), 所以 y^2 ≡ r^2 * s ^2 (mod n), 又因为x ≡ r^2 (mod n),推出y^2 ≡ x * s^2 (mod n), 又因为s^2 ≡ v^-1 (mod n), 所以, y^2 ≡ x * v^-1,并最终推出 x ≡ y^2 * v (mod n),以证实Alice知道y ≡ sqrt(v^-1 * x) (mod n)

上面的协议跑一轮就是单次鉴定,如果随着重复次数越来越多,Alice欺骗Bob的概率会大大减小。直到Bob确信Alice知道s(私钥)。当然,要注意一点,第一步的随机数r随着每轮都需要不同。不能使用相同的。

这个方案协议还可以优化成并行版本,并减少互交轮次,再此不作进一步介绍。

图同构

(To be continued)

C++ 17的特性探索


title: C++ 17的特性探索
date: 2017-08-31 14:38:18
tags:
- C/C++

前言

随着C++ 17标准今年出来以后,各大社区都有C++ 17的讨论,C++的爱好者们都希望这门语言变得更像现代编程语言一样,越来越好用,拥有越来越高级的抽象语义。以下是知乎上的几个讨论:

另外,各大编译器产商也对C++ 17的标准实现进行了跟进。本着垠神的文章《如何掌握所有的程序语言》里面所说,不要关注语言的语法细节,而着重关注学习语言的特性,我打算探索下C++ 17的语言特性,当然,有些特性在其他语言也有对应的概念,比如any,variant,optional等在Java 8中也有了。

Optional

C++17提供了optional这个特性,该特性以类的形式提供,管理一个可选的值。它的常用场景其实没什么高深的,类似于lambda表达式简化了代码,该特性可以用来表示函数的返回值,该返回值可以表现函数可能会调用失败的信息。(注:boost也实现了此特性)

比如你想实现一个把字符串转换为整数的函数,你采用最传统的方法来实现:

bool parse_int(const std::string& s, int& i)
{
   // 实现算法
}

上面这个函数的签名其实并不美观,简洁。第一个参数为需要处理的输入字符串,第二个参数是处理成功时候通过引用的输出参数。最后用了bool返回值来告诉caller是否处理失败。

或者为了减少参数,降低干扰信息,还可以用一个方法,但是还是比较丑陋:

int* parse_int(const std::string& s)
{
   // 实现算法
   // 如果失败,返回nullptr
}

上面的函数用空指针代替了失败的信息。函数签名还是不美观,而且这样的用法极少,需要动态内存分配。很少有开发人员会通过内部new一个int对象,等到caller使用完毕,caller需要自己手动delete:

int *result = parse_int("12334");
if(result)
{
    std::cout << *result << std::endl;
    delete result; // 如果使用完毕忘记释放,内存泄漏
}
else
{
    std::cout << "none" << std::endl;
}

如果使用optional就方便了:

std::optional<int> parse_int(const std::string& s)
{
   int result;
   //....
   if(isSuccess)
   {
       return result;
   }
   return {};
}

int main()
{
     std::cout << "result is: " << parse_int("123456").value_or("invalid paramter") << '\n';
     std::cout << "result is: " << parse_int("dwafawf").value_or("invalid paramter") << '\n';
    
    if(auto result = parse_int("5643"))
    {
        std::cout << "result is: " << *result << '\n';
    }
    
    return 0;
}

以上代码会输出:

result is: 123456
result is: invalid parameter
result is: 5643

还有一个让代码简洁的例子:

template<typename Key, typename Value>
class Lookup
{
    std::optional<Value> get(Key key);
};

Lookup<std::string, std::string> location_lookup;
std::string location = location_lookup.get("waldo").value_or("unknown");

Variant

在C++ 17中以std::variant这个类提供,表示一个类型安全的Union类型。当然,boost有对应的Boost.Variant, Qt有对应的QVariant。 这个类类型的一个实例在给定任何一个时刻保留其中一个类型的值。

设计的动机:

在很多时候,在开发C++程序的过程中,你需要一个类型表示多种类型其中的任何一个类型的时候,你可能相当,union这个关键字来实现以下:

union { int i; double d; } u;
u.d = 3.14;
u.i = 3; // overwrites u.d (OK: u.d is a POD type)

变量u既可以保存int类型的值,又可以保存double类型的值,但是同一个时刻,只有其中一个类型的值保存在其中,但是很遗憾,union关键字一般只支持基本类型,比如int,char,double之类的POD类型,如果用C++使用面向对象的方式编程,以下非POD的类型就不支持:

union {
  int i;
  std::string s; // illegal: std::string is not a POD type!
} u;

于是std::variant就产生了:

 std::variant< int, std::string > u("hello world");
 std::cout << u; // output: hello world
 u = 13;
 std::cout << u; // output: 13

std::variant<int, double> v{ 12 };
std::get<int>(v); // == 12
std::get<0>(v); // == 12
v = 12.0;
std::get<double>(v); // == 12.0
std::get<1>(v); // == 12.0

当然,std::variant确实有点用,但是有点鸡肋的感觉。相比Qt的QVariant相差十万八千里。

Any

在C++17中,这玩意儿以std::any的类提供,表示对于一个任意类型的类型安全的单值容器。当然了,Boost库也有对应的Boost.Any,当然了这玩意儿跟Variant很像,所以在boost的官方文档中,有了这两个类型的对比:

As a discriminated union container, the Variant library shares many of the same features of the Any library. However, since neither library wholly encapsulates the features of the other, one library cannot be generally recommended for use over the other.

That said, Boost.Variant has several advantages over Boost.Any, such as:

  • Boost.Variant guarantees the type of its content is one of a finite, user-specified set of types.
  • Boost.Variant provides compile-time checked visitation of its content. (By contrast, the current version of Boost.Any provides no visitation mechanism at all; but even if it did, it would need to be checked at run-time.)
  • Boost.Variant enables generic visitation of its content. (Even if Boost.Any did provide a visitation mechanism, it would enable visitation only of explicitly-specified types.)
  • Boost.Variant offers an efficient, stack-based storage scheme (avoiding the overhead of dynamic allocation).

Of course, Boost.Any has several advantages over Boost.Variant, such as:

  • Boost.Any, as its name implies, allows virtually any type for its content, providing great flexibility.
  • Boost.Any provides the no-throw guarantee of exception safety for its swap operation.
  • Boost.Any makes little use of template metaprogramming techniques (avoiding potentially hard-to-read error messages and significant compile-time processor and memory demands).

以下是std::any的用法:

std::any x{ 5 };
x.has_value() // == true
std::any_cast<int>(x) // == 5
std::any_cast<int&>(x) = 10;
std::any_cast<int>(x) // == 10

还有更详细的用法:

#include <string>
#include <iostream>
#include <any>
 
int main()
{
    // simple example 
 
    auto a = std::any(12);
 
    std::cout << std::any_cast<int>(a) << '\n'; 
 
    try {
        std::cout << std::any_cast<std::string>(a) << '\n';
    }
    catch(const std::bad_any_cast& e) {
        std::cout << e.what() << '\n';
    }
 
    // advanced example
 
    a = std::string("hello");
 
    auto& ra = std::any_cast<std::string&>(a); //< reference
    ra[1] = 'o';
 
    std::cout << "a: " << std::any_cast<const std::string&>(a) << '\n'; //< const reference
 
    auto b = std::any_cast<std::string&&>(a); //< rvalue reference (no need for std::move)
 
    // Note, 'b' is a move-constructed std::string, 'a' is now empty
 
    std::cout << "a: " << *std::any_cast<std::string>(&a) //< pointer
        << "b: " << b << '\n';
}

从以上代码可以看出来,std::any的使用场景旨在提供类型安全的void* ,你会发现,在很多C/C++开发的系统软件的源码里面都用void* 来传递上下文(context),无论是线程上下文也好还是其他也罢,但是用void* 来传递上下文信息,原来的类型信息就丢失了,到了要使用上下文的时候,需要强制转换成原来的类型,但是万一转换的类型不对,出错怎么办?如果出错,程序会直接崩溃,不会有任何提示,如果使用了std::any在转换的过程中,如果出错,还会以抛异常的方式来提示用户。

所以大多数情况下使用Any,既可以消除void*的隐患问题,又一样的保证了之前void*的低开发成本,一举两得。

std::string_view

这个东西呢,是非真正意义地引用一个字符串,一般用来提供一个字符串之上的抽象,也就是这个view是std::string的一个抽象,可以简单理解为,view是std::string对象的展示层,它不存储实际的数据,只读,不可修改。std::string是Model层,与软件工程中的Model和View对应。

由于以上的一些特点,通常会用到在字符串操作上性能比较苛刻的场景

stackoverflow上也有一个讨论以说明这个库特性的设计动机。

以下是用法:

std::string str{ "   trim me" };
std::string_view v{ str };

v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));

str; //  == "   trim me"
v; // == "trim me"
#include <iostream>
#include <string_view>
int main()
{
    std::string str = "Exemplar";
    std::string_view v = str;
    std::cout << v[2] << '\n';
//  v[2] = 'y'; // Error: cannot modify through a string view
    str[2] = 'y';
    std::cout << v[2] << '\n';
}

在以上的第二个代码片段,可以看到把在Model层的str对象修改了,与它关联的view对象立即内容就随之改变了,反之就不能通过view层的v对象来修改Model层的str对象。如果是C++之前的引用概念的话,就能通过引用来修改原对象了。

以前端开发中的React和Vue的状态管理来说,就是说,View的状态变化与Model的状态变化一致,View的状态随着Model的状态改变而改变。但是View自身的状态改变却不能影响Model的状态改变。这个状态传递是单向的,不是双向的。

std::invoke

这个没多少要说的,就是调用一个callable的对象,还可以传递参数。

Callable对象顾名思义就是,可以像普通函数那样调用的对象,比如std::function等等。

以下为用法:

#include <functional>
#include <iostream>
 
struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_+i << '\n'; }
    int num_;
};
 
void print_num(int i)
{
    std::cout << i << '\n';
}
 
struct PrintNum {
    void operator()(int i) const
    {
        std::cout << i << '\n';
    }
};
 
int main()
{
    // invoke a free function
    std::invoke(print_num, -9);
 
    // invoke a lambda
    std::invoke([]() { print_num(42); });
 
    // invoke a member function
    const Foo foo(314159);
    std::invoke(&Foo::print_add, foo, 1);
 
    // invoke (access) a data member
    std::cout << "num_: " << std::invoke(&Foo::num_, foo) << '\n';
 
    // invoke a function object
    std::invoke(PrintNum(), 18);
}

以上代码输出:

-9
42
314160
num_: 314159
18

然而,还可以再复杂一点,创建一个代理调用函数的模版类:

template <typename Callable>
class Proxy {
    Callable c;
public:
    Proxy(Callable c): c(c) {}
    template <class... Args>
    decltype(auto) operator()(Args&&... args) {
        // ...
        return std::invoke(c, std::forward<Args>(args)...);
    }
};

auto add = [] (int x, int y) {
  return x + y;
};

Proxy<decltype(add)> p{ add };
p(1, 2); // == 3

该模板类可以接收任何函数并作为其代理。

std::apply

这个函数是调用Callable对象并把元组(Tuple)化的参数序列传递给Callable对象,这样就方便了:

auto add = [] (int x, int y) {
  return x + y;
};
std::apply(add, std::make_tuple( 1, 2 )); // == 3

这个特性在类似的函数式语言都会有,比如Scheme中。

类模版参数推导

自动模版参数推导类似已经完成的函数的参数推导,但是现在可以推导模版类构造函数了:

template <typename T = float>
struct MyContainer {
  T val;
  MyContainer() : val() {}
  MyContainer(T val) : val(val) {}
  // ...
};

MyContainer c1{ 1 }; // OK MyContainer<int>
MyContainer c2; // OK MyContainer<float>

用auto声明无类型的模版参数

emplate <auto ... seq>
struct my_integer_sequence {
  // Implementation here ...
};

// Explicitly pass type `int` as template argument.
auto seq = std::integer_sequence<int, 0, 1, 2>();
// Type is deduced to be `int`.
auto seq2 = my_integer_sequence<0, 1, 2>();

Folding表达式

一个folding表达式执行一个封装了对模版参数二元操作的折叠。

  • 一个类似(... op expr)或(expr op ...)这样形式的表达式,其中op为fold操作符,expr为未展开的参数包裹,这样的形式叫一元折叠(unary fold)。

  • 一个类似(expr1 op ... op expr2)这样形式的表达式,其中op为fold操作符,叫二元折叠(binary fold)。expr1和expr2都是未展开的参数包裹,但也可能不都是未展开的。

二元折叠:

template<typename... Args>
bool logicalAnd(Args... args) {
    // Binary folding.
    return (true && ... && args);
}

bool b = true;
bool& b2 = b;
logicalAnd(b, b2, true); // == true

一元折叠:

template<typename... Args>
auto sum(Args... args) {
    // Unary folding.
    return (... + args);
}

sum(1.0, 2.0f, 3); // == 6.0

在花括号初始化列表中的auto推导的新规则

改变了当采用统一初始化语法auto的推导规则。原来,auto x{3}被推导为std::initializer_list类型,现在变为直接推导为int类型

auto x1{ 1, 2, 3 }; // error: not a single element
auto x2 = { 1, 2, 3 }; // decltype(x2) is std::initializer_list<int>
auto x3{ 3 }; // decltype(x3) is int
auto x4{ 3.0 }; // decltype(x4) is double

constexpr的lambda表达式

使用constexpr构造编译时lambda表达式:

auto identity = [] (int n) constexpr { return n; };
static_assert(identity(123) == 123);

constexpr auto add = [] (int x, int y) {
  auto L = [=] { return x; };
  auto R = [=] { return y; };
  return [=] { return L() + R(); };
};

static_assert(add(1, 2)() == 3);

constexpr int addOne(int n) {
  return [n] { return n + 1; }();
}

static_assert(addOne(1) == 2);

lambda以值方式捕获this指针

之前的C++标准只能以引用的方式捕获this指针,现在可以以值来捕获了。因为以前在使用callback函数的异步代码中,必须要求一个合法对象,万一对象超过其生命周期,那么程序就挂了。所以在C++17中*this是对当前的对象拷贝了一个副本,而this还是类似C++11的标准一样以引用捕获:

struct MyObj {
  int value{ 123 };
  auto getValueCopy() {
    return [*this] { return value; };
  }
  auto getValueRef() {
    return [this] { return value; };
  }
};

MyObj mo;
auto valueCopy = mo.getValueCopy();
auto valueRef = mo.getValueRef();
mo.value = 321;
valueCopy(); // 123
valueRef(); // 321

内联变量

在该标准中,inline关键字既能作用于函数也能作用于变量了,作用于变量和作用于函数的语义都是一样的。使用场景嘛,都是为了提高性能。

// Disassembly example using compiler explorer.
struct S { int x; };
inline S x1 = S{321}; // mov esi, dword ptr [x1]
                      // x1: .long 321

S x2 = S{123};        // mov eax, dword ptr [.L_ZZ4mainE2x2]
                      // mov dword ptr [rbp - 8], eax
                      // .L_ZZ4mainE2x2: .long 123

嵌套namespace

这个不必多说,主要是书写代码更简洁了:

// C++ 11
namespace A {
  namespace B {
    namespace C {
      int i;
    }
  }
}

// C++ 17
namespace A::B::C {
  int i;
}

Structured bindings

其实这个特性Python里面也有类似的。这个标准提案的目的是解构初始化,它是这样使用的: auto [x,y,z] = expr。 expr作为一个表达式,它需要返回tuple-like的对象,这个对象的元素必须与x,y,z进行绑定。tuple-like的对象包括std::tuple, std::pair , std::array和聚合结构体。

using Coordinate = std::pair<int, int>;
Coordinate origin() {
  return Coordinate{0, 0};
}

const auto [ x, y ] = origin();
x; // == 0
y; // == 0

constexpr if

这个特性还是相当有用的,让代码实例化依赖于编译时的条件。

template <typename T>
constexpr bool isIntegral() {
  if constexpr (std::is_integral<T>::value) {
    return true;
  } else {
    return false;
  }
}

static_assert(isIntegral<int>() == true);
static_assert(isIntegral<char>() == true);
static_assert(isIntegral<double>() == false);

struct S {};
static_assert(isIntegral<S>() == false);

UTF-8字符字面量

UTF-8终于被纳入标准了:

char x = u8'x'; //被编码为UTF-8

有关于修BUG的一个思考


title: 有关于修BUG的一个思考
date: 2016-08-26 10:14:16
tags:
- 软件调试

主要是在知乎看到龚敏敏大神的一篇文章才引发的思考。文章的评论中有个回复比较有意思:

clang里有个bug,在windows平台上编译出来的dll,类的导出函数都多了个下划线前缀,从2011年开始被喷了4年,无效。ms一push,3天就修好了。(然而那个补丁超级邪恶。检测到下划线就去掉,而不是找到为什么会多一个下划线)

以上的回复我有一些删改。

对此我引发的一些思考是,对于大型的复杂精密的系统软件的BUG修复问题:修BUG是找到出现BUG的本质原因,还是可以在适当的情形下考虑从其他角度规避造成BUG这个现象就可以了?简单来说,就是只要让该现象不出现就可以了。

如果是正常情况下,在我们的工作中这样修复BUG是万万不行的,因为这只是隐藏问题,而不是解决问题,这是一种不负责任的做法。我相信只要是一个负责任的工程师都不会这样做。

但从以上微软修复BUG的例子来看,工程师们选择的是后者。因为编译器是一个很精密复杂的工程,在没经过严格的单元测试下不会发布,所以测试编译器的正确性也是含金量很高的活。对于这种精密复杂的系统修改它的BUG就不是那么容易的,而且又在极其有限的时间内修复,那么就难上加难了,因为很容易牵一发而动全身,一旦出现一些诡异难缠的BUG,即使是经验丰富的工程师短时间内很难找到问题症结所在。

最后,我对这种方式的修BUG行为做个总结,如果要以这种方式解决问题,那么你应该明确评估以下几点

  • 衡量修复BUG的复杂度,真正修复它需要多长时间,技术难度大不大,是否紧急
  • 评估出现该BUG的模块对于整个项目的重要程度,如果短期不修复,会对用户造成多大影响
  • 是否存在朴素笨拙的办法规避BUG现象,如果存在,那么可以。(当然,对于我的工程经验来说,有些时候用笨办法解决问题非常符合实际)

最最后,真的是最后了 = =! 分享一个我平时Debug的方式:

  • 程序业务逻辑采用日志系统输出打印,出现这类的BUG,往往可以通过log就能定位了
  • 出现BUG,首先一定要复现BUG,不然,都不知道怎么Debug。再然后,简化场景分析,不断的验证或推翻自己的想法,最终解决。
  • 出现程序crash的BUG,这种BUG太好了,90%是内存访问违例,空指针或者数组copy越界等这个只要用相关工具分析程序崩溃时候的CoreDump基本就可以解决了
  • 一定不要瞎猜,一定不要瞎猜!!!先Profile!特别是遇到性能瓶颈问题!
  • 明确目标,你要的是解决问题。

我的C语言混乱代码


title: 我的C语言混乱代码
date: 2016-08-27 10:48:27
tags:
- IOCCC
- C/C++
- 混乱代码

其实这段代码是我半年前写的了,现在才把它放到自己的博客上来,就当是记录下吧 :)

IOCCC是国际C语言混乱代码大赛(The International Obfuscated C Code Contest ),大二的时候知道的,当时我也写了好些,不知道放哪里了,刚又想到一个新玩法,虽然还远远达不到IOCCC的级别,不过也将就着看吧,输出我的微信ID。 = w =

#include<stdio.h>
double _[] = { 19910948789782318, 193, 8, 1};
char* __ = (char*)_;
int main(int ___)
{
    _['!' - 30] && _['$' - 35] && (_[0] *= 10, main(--_['$' - '#']));
    *(char*)_ += 5, _['#' - ' '] = 0;
    _['%' - '#'] && (putchar(*__++), main(--_['%' - '#']));  
}

最后附上一些参考吧,CoolShell的博主也写过类似的文章给大家启发:

DNS是怎样工作的?


title: DNS是怎样工作的?
date: 2017-03-09 15:38:56
tags:
- DNS
- 计算机网络
- TCP/IP

直接开始正文算了,主要是总结。

Episode 1-----网站是未知的

先来陈述一个事实,计算机和其他设备在因特网上互相通信识别对方都是通过IP地址进行的。但是人们并不擅长记忆类似于10.0.0.1 192.168.1.0等这样的IP地址,所以就用了字符文字串(google.com, wikipidia.org)

而域名系统(Domain Name System, DNS),就是把IP地址和字符文本串关联在一起的系统,这样就能找到IP地址了。

假设个场景: 小A在浏览器里输入的一串mathxh.com的网址

首先,浏览器和操作系统会去它们各自的缓存中检查是否有mathxh.com的地址,如果没有,那么操作系统会去请求解析器(resolver)

啥是resolver呀?请看下一章

Episode 2-----漫漫长路

当当当,因为前一章节提到,cache里面没有mathxh.com的IP,所以这个请求到resolver了,resolver通常是你上网的ISP(Internet Service Provider)提供,也就是因特网服务提供商,你家办的是电信的宽带吗? 这时候电信公司就是你的ISP。所有的Resolver必须知道一件事:根服务器在哪。

根服务器又知道.com TLD 服务器(Top-Level Domain,顶级域名)在哪里。

等等,Resolver到底是啥?还是没有说清楚,其实Resolver就是我们通常所说的DNS服务器,你需要知道配置DNS server的IP地址来的,通常这个server由ISP提供,当然,也可以采用免费的域名提供商提供的server。比如:OpenDNS。至于,怎么使用,配置下它提供的DNS server的IP地址就可以了。所以**封锁网站都是封IP,不是封域名。对于个别封锁域名的网站,用OpenDNS提供的服务既可以上被封的网站,因为OpenDNS找得倒被封域名的IP啊。

Episode 3-----层级结构的顶层

好了,咱的请求经过询问了根服务器后,得知了COM顶级域名服务器的地址(这个地址会缓存下来,下次就不必找根服务器了)。

然后,刚刚我们请求到达的根服务器只是全球13个根服务器其中的一个。根服务器在DNS层级结构中的最顶层。

全球分散着13个独立的组织,他们与13个根服务器一一对应,这些服务器的名字是以[A-M].root-servers.net的形式存在,字符A-M,刚好是13个。
但是!这不是意味着全球只有13个物理根服务器来支撑整个互联网!这个13个根服务器每一个都会有多份自己的镜像服务器分布在全球各地。

Episode 4-----顶级域名的大杂烩

当当当,我们的请求到达了.COM顶级域名服务器。

先说个题外话:

大部分顶级域名是归一个叫Internet Corporation for Assigned Names and Numbers(ICANN)的组织机构管理分配的。.COM这个顶级域名是世界上最早的一批创建的了,在1985年。现如今已成为互联网上最广泛的域名。

当然了,还有很多其他类型的顶级域名,比如,.jp代表日本,.fr代表法国, .**代表**, 还有广为人知的.net,.org, .edu 。 最后还有一种域名,基础设施顶级域名(InfTLDs),比如, .ARPA, 一般用来DNS反向查找,简单来说就是,从IP地址查域名。

现今,还有很多杂七杂八的顶级域名被建立了:.hot , .pizza, .app, .health等等。

现在回到之前的场景,我们的请求到达了.COM顶级域名服务器,.COM服务器为我们找到了一系列已授权的名字服务器:ns1.mathxh.com, ns2.mathxh.com .... ns6.mathxh.com (可能更多)。

Episode 5-----回家

由前一章节得知,问题来了,那么多个已授权的名字服务器,我到底该与哪个建立连接?(抓耳挠腮)。

简单! 这就需要域名注册商的帮助了。

当买下域名的那一刻,域名注册商就联系顶级域名登记处预定这个名字,并把这个域名注册到已授权的名字服务器上(当然,这名字服务器有很多)。比如,一个域名example.com下面,就有多台对自己负责的名字服务器。

请求会直接去找ns1.mathxh.com的名字服务器,然后使用WHOIS查询,一般电脑上都安装这个工具了,最后,由其中一台nsX.mathxh.com告诉了我们mathxh.com的IP地址。

好了,完工了,请求记住了IP地址该原路返回回家了。请求把带回来的IP地址缓存了下来,以免下次需要使用又需要请求Resolver。最后,把IP地址告诉浏览器,浏览器就对IP地址开始真正请求访问了。

终章--------哎哎? 好像错过了什么

在找到mathxh.com的IP地址之前,是怎么找到ns1.mathxh.com的地址的?
不是要询问ns1.mathxh.com才找到mathxh.com的IP吗?还没有找到主域名的IP,就可以找到子域名ns1.mathxh.com的IP了吗?好矛盾呀。

我们是不可能在找到主域名mathxh.com的IP前,就得到子域名的IP的,无解!

其实实际情况是这样的:
当resolver询问.COM顶级域名服务器的时候,会有一个额外的信息response。这个response内就包含mathxh.com底下至少一个子域名的实际IP地址,所以resolver就知道nsX.mathxh.com的IP地址了。

所以resolver不仅知道子域名的名字,还知道子域名的IP地址,所以就打破了之前无解的循环依赖,由子域名就可以找到主域名的IP地址,主域名也可以找到子域名IP。

CPU缓存效果概要


title: CPU缓存效果概要
date: 2018-02-8 17:16:53
tags:
- 计算机体系结构
- 性能优化

前言

大部分看过《CSAPP》这本经典的人,一般都能理解CPU缓存的大概机制,并能简单通过CPU的缓存机制来优化程序性能。

我这篇文章主要也是用一些代码样例来从一些不同的角度演示缓存的工作机制和实战中的代码优化。

Example 1:内存访问和性能

比较以下两种不同的循环,哪种循环相对更快?(不用管是什么语言的代码)

int[] arr = new int[64 * 1024 * 1024]; // 128MB

// Loop 1
for (int i = 0; i < arr.Length; i++) 
    arr[i] *= 3;

// Loop 2
for (int i = 0; i < arr.Length; i += 16) 
    arr[i] *= 3;

如果不出意外,只要写过代码的人,直观上就可以判断出来,哪段更快,很明显,Loop 2中的循环的步长为16,每16个进行一次乘法运算,而Loop 1中是每一个元素都需要进行乘法运算。显然易见,Loop 2只做了Loop 1的工作量的6%((64/16)/64)。

以上对代码的分析貌似是正确的,但是在现代处理器体系架构下从实际代码运行性能的分析,这两个循环花费的时间差不多(Loop 1是80ms,Loop 2是78ms)。显然Loop 2花费的时间不是Loop 1的6%。为什么? 怎么与设想的不符呢?

原因是,两个循环都在内存访问上花费了大致相同的时间。运行时花费的代价反而是对数组的内存访问上,而不是循环体内的整型乘法运算。

Example 2: 缓存行(Cache Line)的影响

来,下面我们来对以上例子代码进行一次更深入的探索,循环步长的值不再是1和16了,而把其改为一个变量,设为K:

for (int i = 0; i < arr.Length; i += K) 
    arr[i] *= 3;

对K设不同的值,观察循环性能的变化曲线:

由上图看到了,K值在1到16之间,性能的变化不明显,耗费的时间都相差不大。

但是,在16以后,步长每增加2倍,运行耗费的时间几乎减半。为什么?

这种现象背后的原因是当今的现代CPU不会一个字节一个字节的访问内存。相反,CPU它一般会直接把内存中相邻的内存以64字节每块(chunk)组成的块状数据取到缓存中,这种就叫缓存行。(64字节算一个缓存行,缓存行可以一次取多个)。当你在读取一个特定的内存地址上的数据的时候,CPU就会把以当前内存地址开始以后的64字节读入到缓存中,所以访问在一个相同的缓存行中的任何数据,所花费的时间都差不多,而且代价小很多。

讲解到这里,你应该就能理解为什么当步长为1和16时,循环所花费的时间差不多了,因为步长1到16之间的数据都在一个相同的缓存行中,16*sizeof(int)=64字节。 正好是一个缓存行。你可以想象CPU已经把这个128MB的数组全部放到了缓存中,每64字节组成一个缓存行。所以步长为32的时候,每一次循环就访问一个新的缓存行,当步长为64的时候,循环以一次每4个缓存行来访问。

所以理解缓存行的机制,对于程序优化很重要。比如,数据对齐可能会决定一个CPU的操作会触碰到1个还是2个缓存行。如果数据没对齐,那么一次取数可能对于CPU要做2次操作。

Example 3: L1 和 L2缓存的大小

当今计算机,一般都有二级缓存了,L1和L2,甚至还有L3缓存了。如果你想知道不同缓存和缓存行的大小,你还需要用工具和系统提供相关的API来细致分析。因为缓存行真不一定是64字节。windows上你可以通过CoreInfo这个工具来查看缓存详细信息,或者通过调用GetLogicalProcessorInformation这个Win32 API来实现查找缓存相关详情。

D:\MathxH\tools\Coreinfo>Coreinfo.exe -c -l

Coreinfo v3.31 - Dump information on system CPU and memory topology
Copyright (C) 2008-2014 Mark Russinovich
Sysinternals - www.sysinternals.com

Logical to Physical Processor Map:
**--  Physical Processor 0 (Hyperthreaded)
--**  Physical Processor 1 (Hyperthreaded)

Logical Processor to Cache Map:
**--  Data Cache          0, Level 1,   32 KB, Assoc   8, LineSize  64
**--  Instruction Cache   0, Level 1,   32 KB, Assoc   8, LineSize  64
**--  Unified Cache       0, Level 2,  256 KB, Assoc   8, LineSize  64
****  Unified Cache       1, Level 3,    3 MB, Assoc  12, LineSize  64
--**  Data Cache          1, Level 1,   32 KB, Assoc   8, LineSize  64
--**  Instruction Cache   1, Level 1,   32 KB, Assoc   8, LineSize  64
--**  Unified Cache       2, Level 2,  256 KB, Assoc   8, LineSize  64

在我这台ThinkPad破本上用windows相关命令行查看到的缓存信息是,有三级缓存,并且缓存行的大小都是64字节。32KB的L1数据缓存,32KB的L1指令缓存还有3MB的L3缓存等。

好我们再来分析一下下面的代码程序:

``` java
int steps = 64 * 1024 * 1024; // 64MB
int lengthMod = arr.Length - 1;
for (int i = 0; i < steps; i++)
{
    arr[(i * 16) & lengthMod]++; // (x & lengthMod) 相当于 (x % arr.Length)
}

以下是以上代码片段随着数组大小增大的性能曲线图:

因为L1是32KB, L2是256KB,L3是3MB。所以你可以看到大约在数组大小超过32KB,256KB,3MB或4MB以后会有明显的性能下降,也就是耗费的时间越多。

Example 4 指令级并行

现在,我们来看看一些不一样的地方。以下两个循环,谁会更快?

int steps = 256 * 1024 * 1024; //256MB

int [] a = new int[2];

//Loop 1
for(int i = 0; i < steps; ++i)
{
    a[0]++; // op1
    a[0]++;  // op2
}

// Loop 2
for(int i = 0; i < steps; ++i)
{
    a[0]++;  // op1
    a[1]++;  // op2
}

很多人一眼看上去貌似是一样快,但是我几乎在所有的机器上测试过,几乎都是Loop 2快过Loop 1。为什么? 这就需要认真分析两个循环体里面的操作依赖性了。

在Loop 1的循环体中op1和op2是有依赖关系的,op2的操作执行必须等到op1完成才能开始。而Loop 2的循环体中,op1和op2没有依赖关系,理论上可以同时完成,这个就为处理器提供了优化的可行性。

现代处理器对Loop 2这种情况有多种的并发处理方式,他可以在L1缓存中同时访问a[0]和a[1],并同时执行op1和op2这两个算数运算。但是在Loop 1中,处理器就没有机会利用指令级并行了。

Example 5 缓存相联

在缓存的设计中,有一个关键的决定就是是否要把主内存的每个chunk都存储到每个缓存中,或者只是选择多个缓存的其中几个。

在现代处理器体系中,就有如下几种方式来把多个缓存映射到主内存的块中:

  • 直接映射缓存,每个内存块只可能被映射到其中一个特定的缓存槽中,是一对一的关系。这种算法就简单的实现方式就是,把主内存的块的index映射到缓存中(chunk index % 缓存槽数)。块下标对缓存槽数取模运算。两个内存块不可能同时都存放在一个相同的缓存槽中。

  • N路相联缓存(组相联缓存),每一个主内存上的块能被存储到N个特定缓存槽中的其中一个槽中。例如,在16路相联缓存中,每个内存块被存储到特定的16个不同的缓存槽中(16个槽的其中一个)。通常的实现是块的index的最低序列位来表示16个不同的槽

  • 全相联缓存, 每个内存块可能会被存储到任何一个缓存槽中。类似于操作Hash表这样的数据结构。

直接映射缓存容易造成冲突,因为可能会有多个内存块都被映射到相同的内存槽中了,这样会造成缓存频繁的抖动(内存块不停地换入换出,命中率下降)。另一方面,对于全相联缓存比较复杂,硬件实现成本也高。所以N路相联缓存是现代处理器缓存典型的解决方案,它在实现成本和友好的命中率上做了很好的折中和取舍。

例如,在我的电脑上,3MB的L3缓存就是12路相联,32KB的L1缓存和256KB的L2缓存都是8路相联。对于L3缓存,所有的64字节的内存块都会基于自身的index被映射到L3缓存中的特定的12个不同的槽中。

因为L3缓存有4096(2^12 = 4096)个槽,那么每个集合就有12个槽相联,那么就有341个集合。2^8 约等于 341 。 所以chunk index的低8位就决定了这个块存放在那个集合中。

显然,缓存的相联方案也会影响性能,但是这里就不再深入,这方面知识也不是本篇文章的重点。还有一个原因就是我目前暂时不能给大家讲解清楚。

Example 6 缓存行伪共享

在多核机器上,缓存就有遭遇到另一个问题-----并发一致性。不同的核一般来说都有属于自己的缓存。我的机器上L1一般都有属于自己的核,L2是多个核共享的缓存。在现代多核架构中缓存一般有多个层级,速度越快越“接近”核的缓存约属于单个核所有。

当一个核修改了它自身缓存中的一个值的时候,那么其他处理器的核就不能再使用原来的值了,这就导致了缓存失效。

为了演示这个问题,考虑下面的代码样例:

private static int[] s_counter = new int[1024];
private void UpdateCounter(int position)
{
    for (int j = 0; j < 100000000; j++)
    {
        s_counter[position] = s_counter[position] + 3;
    }
}

以上这段代码如果在多核机器上,用四个不同的线程分别执行UpdateCounter(0),UpdateCounter(1),UpdateCounter(2),UpdateCounter(3)等到四个线程的结束时所花费的时间记为T1。

如果你用四个线程分别执行UpdateCounter(16),UpdateCounter(32),UpdateCounter(48),UpdateCounter(64)等到四个线程结束时所花费的时间记为T2。

这两种不同的方式比较性能,T2比T1少!

为什么? 方法一中的0,1,2,3所取的数据几乎都在一个缓存行中,其中一个核每更新计数一次,那么整个缓存行就失效了,那么其他核取数据就cache miss了。如果这样,需要关闭缓存机制。方法二中,是因为不同线程所取的数据在不同的缓存行中,这样就提高了并发量

Example 7 硬件复杂性

好吧,即使你完全了解之前所讲述的缓存机制,但是在实战中,还不一定照葫芦画瓢就能轻而易举通过优化程序提高性能,一些个别的硬件体系在有些时候还是会使你震惊,不同处理器架构的优化手段也不尽相同。

比如,在有些处理器架构中,L1缓存可以存在并行(如果从不同卡槽来访问),如果是来自相同卡槽的访问那么L1缓存就可能是顺序访问了。有时候,处理器作出的优化,也能让你吃惊,你的优化是多余的。 :)

总结

还是不喜欢通过缓存特性来优化程序,虽然很多高性能的网络框架用到了这些缓存的原理,但是我还是喜欢算法级别的优化。原因就是Example 7,由于硬件的复杂性,很难写出跨平台的缓存利用友好的高性能程序,也很难根据硬件特性来预测程序性能。

有关于即时通讯的开发要点


title: 有关于即时通讯的开发要点
date: 2017-09-10 09:26:00
tags:
- 即时通讯
- 消息推送

前言

现在互联网应用如火如荼,微信,QQ等产品就先暂时不提了。每个移动端App的应用几乎有自己的消息推送系统,还有传统的电商网站京东,淘宝等都有自己的聊天工具,阿里旺旺,或者可以直接在Web端发送消息给卖家。等等这些产品都用到了IM(Instant Messaging)技术。下面我会通过我经过网络上的资料,整理转变我自己的理解,以及对这方面相关开发的的见解和看法。当然,这篇文章是综述性的文章,不会讲解到每个细节,我只能尽力而为。

从UDP和TCP开始

对于有网络Socket编程经验的开发者来说,使用何种传输层协议来实现数据的通信是一个非常基础的问题。从PC时代的IM开始,IM开发者们在为数据传输协议的选型争论不休。到了移动互联网时代,鉴于移动网络的不可靠等特点,再加上手机的省点策略,流量压缩等,为这个问题的回答增加了更多的不确定因素。

在分析到底使用UDP还是TCP之前,有必要先讨论一下互联网与移动互联网的网络环境特点。

互联网的网络基础建设,经过十几年长期的发展,已经较为稳定和成熟,PC终端、操作系统的能力也达到了较高的水平。

而移动互联网,由于涉及到无线电话网络基站、2G、3G和4G技术的不断发展,其稳定性、带宽、资源分配等各方面虽日趋完善,但当前终究还有不少问题的存在。另外,由于移动互联网其“移动”的本质,加上智能终端设备(智能手机、平板电脑)的发展较晚,目前还在不断演变的情况,与互联网相比,移动互联网还是低速、不稳定、终端能力稍弱的情况。而且由于其“移动”本质,短时间内很难达到互联网的质量。

所以,在互联网的环境里面,网络应用程序由于网络设施、操作系统的成熟,开发使用起来比较容易,资源也较为充足。而移动互联网还是要“斤斤计较”。

智能移动终端电池续航能力和系统休眠

智能终端设备的电池续航能力始终是技术瓶颈。在连续使用的情况下,绝大部分智能设备电池无法支持两个小时以上。所以在没有外部电源的情况,智能终端设备必须频繁、长时间休眠,这将极大地影响两种网络环境下的网络应用场景。

IPv4资源,端口资源

这个话题往往被很多人忽略,但它有着至关重要的影响。虽然大部分人都很清楚IP地址的紧缺导致的动态IP分配的必然,却忽略了由于IP地址不足引起的端口资源不足。

由于需要动态分配IP地址(这里不仅仅指互联网入口的IP,还包括局域网内部的IP),路由器的工作原理都是经过端口映射,把内部网络(包括PC、手机、平板、Wifi、2G、3G、4G)IP与端口映射成外部IP(通常是公网IP)和对应的端口,并维持这个映射关系,才能正常地修改、转发报文信息,保证内部各个ip、端口与外部的各个ip、端口的通信。

然而,单个IP地址的端口资源是有限的,理论上限是65535个端口。对于普通宽带路由器来说,这个已经很充足了。但是!对于大型的网络服务、网络主干接入点等来说,如果IP资源不足,每个IP几万个端口的资源很快会耗尽,从而影响正常通讯。

端口映射老化时间

正因为如此,所有的路由器都会为每个端口映射关系设置老化时间,如果老化时间过期,则端口映射关系失效,该端口被释放给其他连接使用。如果端口全部耗尽,则无法再新建内部与外部的网络连接。

端口映射老化时间,比很多人想象中的要短很多。一般的家用宽带路由器,老化时间一般是两三分钟;在有线宽带运营商接入部分,老化时间可能少于两分钟。在无线电话网络运营商接入部分(例如GPRS连接),老化时间甚至不超过一分钟!

也就是说,任何一个网络通讯(不管是TCP或UDP),如果几分钟之内没有网络报文传输,其占用的IP地址端口将被路由器回收。这个时候该次通信必将终止,不管TCP还是UDP,那么都是浮云。

更残酷的事实是,互联网可认为是由无数个路由器连接而成的,一个网络通信往往需要通过n个路由器,每个路由器都会为一次通信建立自己的端口映射。只要其中一个路由器回收其端口,则整个通讯中断。

这也是很多人疑惑为什么TCP的KeepAlive参数无法保证长连接的原因。TCP的KeepAlive默认是两个小时(而且该参数还是TCP的可选实现,不是必然实现),在路由器端口映射老化时间的影响下,必然无法发挥其作用。实际上,该参数在单一的局域网内才可能被使用上,还要依赖具体的操作系统。

由于路由器端口映射的存在,加上智能终端频繁、长时间的休眠,TCP长连接的实用性在移动互联网情况下极大地打了折扣。

也因为如此,移动端IM、推送系统必须实现所谓的心跳包机制,以保持端口映射关系的老化时间不会减少到0而被回收,从而避免连接中断。

服务端承载能力

不管是UDP还是TCP,最终都是应用服务端的设备去提供服务的。而TCP由于提供了安全可靠的流服务,其对计算机、网络资源的消耗是远远大于UDP协议的。对于配置较好的主流服务器,配备大量的内存(数十G至上百G内存),与高速的磁盘、网卡,是能同时支持数百万个TCP连接的。不过这里需要较专业的服务器设置,需要调整不少系统参数,再加上服务程序的配合。另外,TCP连接的建立、维持与释放,都是需要较昂贵的计算、网络资源的。

终端在线服务,若是一个较为简单的服务,未必使用上TCP众多的高级功能,但承受TCP的昂贵成本,未必值得。如果能用UDP来提供服务,单服务器的承载能力,是可以去到TCP服务的数十倍,甚至上百倍的增长。这也是为什么DNS这种并发数巨大的服务器提供UDP接口的原因。

另外,上百万TCP连接的网络服务,其编程的难度、程序复杂度、调试难度、服务器运维成本、网络成本等都远远高于UDP。

而UDP编程,与上百万个终端通讯的难度与成本则低很多。如果提供的网络服务不是基于流的服务,也允许一定的失败机率(例如P2P),则UDP往往是更适合的方式。

QQ的选择

从最经常接触的QQ来说,它既有UDP又有TCP,最终登录成功后,QQ都会有一个TCP连接来保持在线状态。这个TCP连接的远程端口一般是80,采用UDP方式登录的时候,端口是8000。

UDP协议是无连接方式的协议,它的效率高,速度快,占资源少,但是其传输机制为不可靠传送,必须依靠辅助的算法来完成传输控制。QQ采用的通信协议以UDP为主,辅以TCP协议。由于QQ的服务器设计容量是海量级的应用,一台服务器要同时容纳十几万的并发连接,因此服务器端只有采用UDP协议与客户端进行通讯才能保证这种超大规模的服务。

QQ客户端之间的消息传送也采用了UDP模式,因为国内的网络环境非常复杂,而且很多用户采用的方式是通过代理服务器共享一条线路上网的方式,在这些复杂的情况下,客户端之间能彼此建立起来TCP连接的概率较小,严重影响传送信息的效率。而UDP包能够穿透大部分的代理服务器,因此QQ选择了UDP作为客户之间的主要通信协议。

采用UDP协议,通过服务器中转方式。因此,现在的IP侦探在你仅仅跟对方发送聊天消息的时候是无法获取到IP的。大家都知道,UDP 协议是不可靠协议,它只管发送,不管对方是否收到的,但它的传输很高效。但是,作为聊天软件,怎么可以采用这样的不可靠方式来传输消息呢?于是,腾讯采用了上层协议来保证可靠传输:如果客户端使用UDP协议发出消息后,服务器收到该包,需要使用UDP协议发回一个应答包。如此来保证消息可以无遗漏传输。之所以会发生在客户端明明看到“消息发送失败”但对方又收到了这个消息的情况,就是因为客户端发出的消息服务器已经收到并转发成功,但客户端由于网络原因没有收到服务器的应答包引起的。

UDP包中的一个包的大小最大能多大?

既然大部分都采用了UDP来实现IM,那么UDP包的大小应该设置到多大呢?因为UDP是无连接的,它不需要TCP的3次握手,直接向特定IP和端口上扔数据就可以了,不管对方是否开启了服务。而且UDP包是有边界的,一次数据互交就是一次UDP包传输,没有拆包的概念,但是由于它的不可靠,UDP包到达目的地包的顺序可能会乱掉,或者丢失。所以一次UDP包的发送如果数据太多的话,用UDP实现可能就不优美。

学过计算机网路都都知道,工业界主流的都是采用TCP/IP协议栈,总共有四层,从上到下是,应用层,传输层,网络层,数据链路层。UDP与TCP是通用在传输层的协议。所以以下两个结论是很多开发人员的总结。

结论一:局域网环境下,建议将UDP数据控制在1472字节以下

以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的,这个1500字节被称为链路层的MTU(最大传输单元)。但这并不是指链路层的长度被限制在1500字节,其实这这个MTU指的是链路层的数据区,并不包括链路层的首部和尾部的18个字节。

所以去除头部和尾部,那么实际上这个MTU就是网络层IP数据报的长度限制。由于IP数据报文本身也有头部20字节,所以IP数据包的数据区长度最大为1480字节。而这个1480字节就是用来存放TCP或UDP数据报文的。

因为IM大部分采用了UDP传输,所以又去除UDP报文的头部8个字节,所以UDP数据报的数据区最大长度为1472字节。这个1472字节就是我们一次UDP Socket所发送的最大字节数。也就是说,聊天消息的内容最好不要超过1472个字节。

如果我们发送的聊天内容超过了1472个字节了呢? 换句话说就是在数据链路层超过了1500字节的MTU长度呢?这个时候发送放的网络层(IP层),就会把这个内容进行分片(fragmentation),把数据报文分成若干片,使每一片都小于MTU,而接收方的网络层需要进行报文重组。这样麻烦的事情就来了,在这里你可能会有疑问,这些都是TCP/IP协议栈帮你实现好了啊,IP层会自动拆分和重组啊,有什么好担心的?应用层根本不用管嘛。如果你这样想,那么就大错特错了,因为UDP的特性,当由于一次发送超过MTU进行分片发送时,如果在传输过程中某一片数据丢失了,那么接收方就无法重组IP数据报文,那么将丢弃整个UDP数据报,简而言之就是,这次发送完全失败,接收方传输层完全看不到。如果网络环境很烂,分片数据会经常丢失,那么对于用户来将,发送方发10次聊天内容,接收方有9次收不到。

结论二:Internet编程时,建议将UDP数据控制在548字节以下

因为Ineternet上的网络环境更加复杂和拥堵,而且Internet上的路由器可能会将MTU设置为不同的值(路由器一般来说是三层设备,实现了物理层,链路层,网络IP层。所以可以设置数据链路层的MTU)。

鉴于Internet上的标准MTU为576字节,所以在进行Internet上的UDP编程时,最好将UDP的数据区长度控制在548字节以内,548字节怎么得来的?还是结论一的算法,576 - 20 - 8 = 548。

IPv4标准 指出, IP报文中的Total Length字段说明有以下文字:

All hosts are required to be able to reassemble datagrams of size up to 576 bytes

也就是说,IPv4规定IP层的最小重组缓冲区大小为576字节,也就是超过这个字节数就重组,一旦重组就会大概率丢失。所以本质原因很可能不是Internet上的标准MTU是576个字节。

应用层采用什么数据传输格式

之前讨论过了,传输层尽量采用UDP传输,但是基于UDP报文的数据区的格式应该采用什么呢?这个阶段的选择也比较有争议。

精要分析,原因大概在以下三点:

  • 可选择的协议或封装格式多种多样:XMPPProtobufJSON、私有2进制、MQTTXML、类似与http协议的纯文本方式等等;

  • 同一种格式并不能适用于大多数的场景: 不同的场景有同的考虑而协议的选择往 跟这是挂钩在一起的,比如:移动端IM或推送技术用XMPP这样的协议时,多数情况下都会被喷

  • 开发者对所选格式有各自的偏好: 有的人或团队对某种或某几种格式有不一样的经验和技术积累,也促成了他们对某种或某几种协议的偏好。

当然,这些协议的选择应该还是具体问题具体分析,该选什么协议由场景决定、由团队的技术积累决定、甚至由项目的周期和成本决定,这里不存在唯一解,只有最适合的数据传输格式,不存在最好的格式一说。

数据格式的选择需要考虑的方面

1.网络数据大小:占用带宽,传输效率

虽然对单个用户来说,数据量传输很小,但是对于服务器端要承受众多的高并发数据传输(尤其现时高并发、大用户量的IM聊天应用和实时推送服务端等场景),必须要考虑到数据占用带宽,尽量不要有冗余数据,这样才能够少占用带宽,少占用资源,少网络IO,提高传输效率。

2.网络数据安全性:敏感数据的网络安全

对于相关业务的部分数据传输都是敏感数据,所以必须考虑对部分传输数据进行加密。这通常出现在银行等数据安全性要求很高的应用行业和场景里,当然传统的即时通讯应用里基于用户隐私考虑,数据加密也是同样是个必须考虑的问题。安全性是应用的基础条件,需求是一样的,只是加密程度、安全性级别要求有不同而已。

3.编码复杂度

编码复杂度包括序列化和反序列化复杂度、效率、数据结构的可扩展性和可维护性,开发人员编码工作量。

对于平台相关业务的代码实现也需要考虑到数据发送方和数据接收方数据处理的复杂度和数据结构的可扩展性,可维护性,人力成本和实施复杂度也必须考虑在内。通常情况下,即时通讯应用(比如IM聊天应用)在开发的前期,为了方便调试,很多团队会用简单的文本协议、JSON等能直观查看的方式,但后期生产部署后,为了流量等考虑,可能会转用Protobuf等更省流量的协议。但总之,协议的定义不可能永远一成不变,但如果在实现的时候就有这些预见性,相性会大大减轻未来的运营风险。

4.协议通用性、大众规范

数据类型必须是跨平台,数据格式是通用的,大家普遍能接受上手的。当然,现在已经迈入移动互联网时代,多端、多平台、异构平台的数据通讯是先决条件,而协议的选择,通用性也最多只是应用层有区别。当然,无论如何,异构平台的一致性,是毫无争议的必备条件。

不同类别的数据传输协议格式的对比


1.自定义二进制

像大多数嵌入式网络应用开发的公司一般都会选择自定义应用层二进制协议,比如自己定义协议头,协议正文。那个时候还需要开发人员自己手工序列化,把主机字节序转换为网络字节序,或者反之。协议规范小,这没问题,一旦很大,需要耗费很大精力和时间。如果用protobuf这样的序列化库就轻松很多了。

优点:信息体积小,占用带宽小,传输效率高,即使不加密,破解难度也高。
缺点:编码复杂度高,自己定义消息格式,自己编写序列化和反序列化方法,自己进行容错处理,可扩展性不强,比如添加个字段,就必须改两端的逻辑处理。

2.提供序列化和反序列化库的开源协议

比如Potobuf,json,还有Apache的 Thrift

优点:支持多语言,通用流行的数据格式,扩展方便,库都支持序列化和反序列化,错误处理也库也支持。信息体积小,占用带宽小,传输效率高。
缺点: protobuf其实本质上也是自定义二进制协议,只不过是库帮开发人员做了序列化和反序列化的工作,而且还有容错处理。如果要说缺点,那么就是调试不方便,抓包的时候不直观,可视化不强。

3.纯文本协议

比如XML,json,类http自定义纯文本格式,SIP。
优点:支持序列化和反序列化,错误处理库也支持,调试方便,可视化强。
缺点:相对于二进制存储占用体积大

所以归而结网,到底怎么选择呢?尽量选择方便调试,可视化强的并且库支持的数据格式。现在XML已经不是主流了,主流网站提供的API,基本都是http传输JSON格式的数据。无非就是延迟性高点,传输效率低点。

  • 自定义二进制格式太复杂了,整个过程都在设计协议消息格式,通过网络库或者Socket的read,write的过程过于复杂,容易出错,如果是基于TCP协议设计的二进制格式,开发人员还必须自己编写代码对TCP拆包,对于很多数据互交的程序,会话费大量时间调试,关注底层细节。

  • 自定义二进制格式不便于扩展,增加,删除协议字段客户端和服务端逻辑都得更改。

  • 如果真选用JSON这样的文本化格式,占用网络带宽这样的问题,其实可以通过数据压缩等手段来解决,JSON本身格式并不复杂,传输效率不低。

当然,如果是我自己的选择,综合考虑我会选择protobuf。因为它支持C++,Java,Python语言等。而且用其作为传输格式,带宽占用与自定义二进制格式只少不多,传输效率很高。对于消息数据大小很敏感的应用是个很好的选择,据说手机QQ的数据传输协议就是使用的protobuf。我以前有个大学同学在的一家手机游戏移动端与服务端的数据传输也是采用的protobuf,基本上不用采取第三方的额外数据压缩手段来调优。从各方面的权衡都好于蘑菇街开源的企业级IM应用---TeamTalk

根据网络上其他开发人员的实际测试,用protobuf序列化后的数据大小是json的10分之一,XML格式的20分之一,自定义二进制序列化的10分之一。基于占尽了各种优势,在效率、数据大小、易用性之间取得了很好的平衡,只有一个调试可视化不强的缺点了。

protobuf的易用性在于,它采用了某种定义结构化的消息格式的语言定义消息格式,然后通过protobuf的命令行工具生成序列化和反序列消息的类,非常轻松。与大多数主流RPC框架一样的使用方式。只不过更加底层一点罢了。

IM客户端设计面临的问题

前面讨论的技术选型也差不多了,接下来就要考虑IM客户端的问题了。这里所说的客户端包括PC的客户端,手机移动端,还有Web端。主要是这三种客户端。

P2P传输还是服务器中转?

IM的通讯方式无非两种:设备直连(P2P)和通过服务器中转

1.P2P方式

P2P多见于局域网内的聊天工具,典型的应用有:飞鸽传书,飞Q等。这类软件在启动后一般做两件事:

  • 进行UDP广播: 发送自己信息和接受同局域网内其他端信息
  • 开启TCP监听:等待其他端进行连接

P2P的方式有种种限制和不便,一方面,它只适合在线的点对点消息传输,对离线,群组等业务支持不够。另以方面,由于NAT的存在,使得局域网内机器互联难度大大提升,在某些网络类型(对称NAT)下无法建立连接。

2.服务器中转

因为IM通讯工具和相关云服务都是建立在互联网上的,所以几乎所有市面上的IM产品(QQ,微信,YY语音等等)都采用服务器中转这种方式进行消息传输,相对于P2P方式,它有如下优点:

  • 能够支持更多P2P无法支持或支持不好的业务,如离线消息,群组,聊天室服务
  • 方便业务逻辑的扩展和新旧版本的兼容

当然,也有缺点,就是服务器架构复杂,并发要求高,团队成员技术要求高。

采用的通讯技术

因为之前讨论过UDP和TCP的选型,所以这里不过多讲解,目前主流的IM网络通讯技术有三种:

  • 基于TCP长连接
  • 基于Http端连接轮询pull
  • 基于UDP报文应答

第二种常见于Web端的IM客户端,它的优点是实现简单,方便开发上手,问题是流量大,服务器负载较大,消息实时性无法很好的保证,对大规模的用户量支持不够,比较适合小型的IM应用。当然,现在Html5标准下,有了WebSocket这样的全双工协议,服务器可以主动向Web端推送消息了,所以对于Web端优先考虑WebSocket协议。由于WebSocket协议比较复杂,是二进制协议,开源的Socket.io就是为了降低WebSocket协议使用难度了进行了封装。

基于TCP长连接能够更好的支持大量用户,问题是客户端和服务器实现比较复杂。也有一些变种,如下行消息推送使用MQTT进行服务器通知/消息的下发,上行使用Http短链接进行指令和消息的上传。这种变种方式能够保证下行消息的及时性,但是在弱网络环境下,上行慢的问题还是比较严重。

基于UDP报文的应答是保证消息QoS机制的关键要点,因为UDP是不可靠的,所以要在应用层做消息重发和应答保证消息可靠性。

其他不可忽视的问题

1.协议加密

这个不过多讲解。

2.掉线重连

由于之前提到过的端口,UDP等老化机制,连接不可能一直有效,在失效的时候必须要重新连接服务器,特别对于之前版本的iOS APP而言并没有真正的后台程序,所以在每一次APP启动的时候都需要一次重连登录。移动端的网络环境随时不稳定,所以更需要考虑此问题。

3.连接保持(心跳机制)

一般IM客户端实现连接保持的方式无非是采用应用层的心跳,通过心跳包的超时和其他条件来进行重连机制,也防止了之前提到的UDP等端口老化的问题。这里懂计算机网络的人可能会问了,TCP协议有KeepAlive这个设置选项,设置为KeepAlive后,客户端每隔N秒(默认7200秒)会向服务器发送一个心跳包。但是这个是理想状况,做工程就是解决很多不是理想状况的问题,因为从实际角度考虑:

  • 1.KeepAlive对服务器负载压力比较大
  • 2.Socks代理对KeepAlive并不支持
  • 3.因为网络本身是不可靠的,在部分复杂情况下,如路由器挂掉,网络抖动,网线可能被直接拔出

基于以上特点,有必要在IM客户端设计应用层的心跳机制。如果客户端是移动端一般会在心跳包上做一些优化:

  • 1.精简心跳包,保证一个心跳包大小在10字节以内
  • 2.心跳包只在空闲时发送
  • 3.根据App前后台的计算压力,调整心跳包发送间隔

所以之前说的QQ用UDP发送接收消息,用TCP来告知服务器客户端的在线状态。这样的设计是合理的。

如果到现在还认为在TCP的IM中没有必要用心跳保活,那么我还可以详细分析其必要性。

对于客户端而言,使用TCP长连接来实现业务的最大驱动力在于:在当前连接可用的情况下,每一次请求都只是简单的数据发送和接受,免去了 DNS 解析,连接建立等时间,大大加快了请求的速度,同时也有利于接受服务器的实时消息。但前提是连接可用。

如果连接无法很好地保持,每次请求就会变成撞大运:运气好,通过长连接发送请求并收到反馈。运气差,当前连接已失效,请求迟迟没有收到反馈直到超时,又需要一次连接建立的过程,其效率甚至还不如 HTTP。而连接保持的前提必然是检测连接的可用性,并在连接不可用时主动放弃当前连接并建立新的连接。

基于这个前提,必须要有一种机制用于检测连接可用性。同时移动网络的特殊性也要求客户端需要在空余时间发送一定的信令,避免连接被回收。

而对于服务器而言,能够及时获悉连接可用性也非常重要:一方面服务器需要及时清理无效连接以减轻负载,另一方面也是业务的需求,如游戏副本中服务器需要及时处理玩家掉线带来的问题。

而TCP的KeepAlive无法代替应用层心跳保活机制的本质原因还是网络本身的不可靠,之前提到的第三点是最重要的。因为TCP是一个基于连接的协议,其连接状态是由一个状态机进行维护的,连接完毕后,通信双方都会处于 established 状态,这之后的状态并不会主动进行变化。这意味着如果上层不进行任何调用,一直使 TCP 连接空闲,那么这个连接虽然没有任何数据,但仍是保持连接状态,一天、一星期、甚至一个月,即使在这期间中间路由崩溃重启无数次,网络抖动,网线插拔无数次,那么TCP的状态机还是保持established的状态,连接还是处于逻辑上连接的状态。比如,现实中经常会遇到例子:当我们SSH到特定的VPS上,不小心踢掉了网线,此时的网络变化不会被TCP检测出来,当我们重新插回网线,仍然可以继续正常使用SSH,同时此时并没有发生任何TCP的重连。

以上的举例意味着,TCP的KeepAlive机制没法检测出来物理网线掉线的情况,它可能会认为一直逻辑上连接着,但是事实上服务的提供方已经不能提供服务了,因为网线被踢掉了,数据链路层已经断开掉了。所以,KeepAlive机制和应用层心跳机制是有区别的,KeepAlive机制是用于检测连接的死活状态(是否有效),而心跳机制则附带一个功能就是检测通信双方的软件的存活状态,是否正在工作运行。

考虑一种实际场景,某台服务器因为某些原因导致负载很高,CPU 100%。无法响应任何请求,但是TCP的KeepAlive机制发送的KeepAlive探针知道并确定这个连接还活着,但是显然服务器已经不算正常运行了,不能提供服务了。而这种情况对于客户端而言,这时最好的选择就是主动关闭TCP Socket断线后重连其他服务器,而不是一直认为当前服务器是可用状态。当然,一般采用TCP长连接的分布式RPC框架都内置这个负载均衡的路由功能了,也有心跳机制,所以业务层开发人员不需要当心。

下面有心跳保活的参考实现方案:

最简单粗暴的做法当然是定时心跳,如每30秒客户端发送心跳一次,15秒内没收到心跳回包则认为当前连接失效,断开连接并重连。这种做法简单直接。如果是PC端这没问题,但是对于手机移动端就麻烦了,这样做比较耗电和耗流量。以一个心跳协议包5个字节计算,一天24小时,就有1460=840分钟,一天就有84060=50400秒,那么一天就有50400/30=1680个心跳客户端需要发送,然而,还需要接收服务端返回来的心跳应答,那么总共需要收发16802=3360个心跳包,那么一天就要耗费33605=16800字节流量,一个月就是16800*30/1024/1024=0.48MB流量,如果多装几款IM软件,每个月光心跳就可以耗费几兆流量,更不用说频繁的心跳带来的电量损耗了。

既然频繁心跳会带来耗电和耗流量的弊端,改进的方向自然是减少心跳频率,但也不能过于影响连接检测的实时性。基于这个需求,一般可以将心跳间隔根据程序状态进行调整,当程序在后台时,尽量拉长心跳间隔,5 分钟甚至10分钟都可以。

而当移动 App 在前台时则按照原来规则操作。连接可靠性的判断也可以放宽,避免一次心跳超时就认为连接无效的情况,使用错误积累,只在心跳超时 n 次后才判定当前连接不可用。当然还有一些小 trick 比如从收到的最后一个指令包进行心跳包周期计时而不是固定时间,这样也能够一定程度减少心跳次数。

4.消息的可达性(QoS机制)

Qos即Quality of service,翻译过来就是服务质量。在移动网络下这个消息可达更加困难,因为移动端网络,丢包,重连等情况非常之多,为了保证消息的可达,需要做消息的回执和重发机制。比如易信,每条消息会最多有3次重发,超时时间为15秒,同时在发送之前会检测当前连接状态,如果当前连接并没有正确建立,缓存消息且定时检查(每2秒检测一次,检查15次)。所以一条消息在最差情况下会有2分钟左右的重试时间,以保证消息可达。

因为重发的存在,接收端偶尔会收到重复的消息,这种情况下就需要接收端进行去重。通用的做法是每条信息都携带上自己唯一的message id(一般是UUID)。

对于消息可达性,也就是Qos机制的实现又是一个非常大的话题,需要自己分析理解。所以之后再另写一篇新的文章来巩固Qos的知识,这篇文章至此为止就完结了。

实现一个简单的高性能布隆过滤器


title: 实现一个简单的高性能布隆过滤器
date: 2019-03-17 12:26:00
tags:
- Bloom Filter
- 数据结构与算法

布隆过滤器是什么

它是一个数据结构,而且一般传统的科班的算法书不会讲到。它可以用来判断某个元素是否在集合内,具有执行速度快和内存占用小的特点。

布隆过滤器高效的插入和查询代价就是:Bloom Filter是一个基于概率的数据结构:它只能告诉我们一个元素的以下两种查询结果:

  • 绝对不在集合内
  • 可能在集合内

针对第二点,也就说可能会有false positive。朴素的布隆过滤器一般是不支持删除操作的,但是也有针对具体应用而产生支持删除操作的布隆过滤器。这个延伸问题我不打算涉及,感兴趣可以查找下相关论文。

布隆过滤器的简单原理以及实现

基本原理

布隆过滤器的一个基础的数据结构就是一个Bit Vector 。 其原理非常简单,这个bit 数组是带有下标索引的,一个索引对应一个bit状态,实际就是0或1。 一般状态1就是元素存在的标识,状态0就是元素不存在的标识。

00000000000100010100101010101010101010101010100101010000001111100111

以上的文本示例就是一个长度为N的比特向量,当然初始状态但是是全为0,但一旦向布隆过滤器插入一个元素的时候,需要对这个元素进行多次hash运算,一次运算的结果为bit vector的一个下标索引,然后把这个索引对应的bit状态设置为1,当多次Hash运算计算下标完成并设置为1,这样元素就算插入完成了。查询的时候也是把元素进行多次hash运算,然后检查对应索引上的bit状态是否为1,如果多次运算后的bit为1就是可能存在,如果其中有一个bit为0,就是绝对不存在。

以上就是布隆过滤器的最基本原理了。是不是很简单? 但是要对其进行分析就需要一定难度了,其中需要你懂些基本的数学知识,比如如果你要设计一个布隆过滤器,以下几个方面需要你来取舍:

  • 长度为m的比特向量,这个长度需要多长?m需要多大?也就是说需要几个bit?

  • 进行多次hash运算,这个多次是需要不同的k个Hash函数的,那么这k个Hash函数怎么选择? MD5,SHA256还是其他?这个K的最优值又是什么?

  • 已知当前比特向量已经插入n个元素了,那么过滤器判断的误报率(false positive)又是怎样的?毕竟是概率性数据结构。

如果以上三个要点实现过滤器的设计者没有考虑清楚,很可能他设计出来的过滤器根本不可用,误报率上升到一定程度就没有意义了。

Hash函数的选择

布隆过滤器里的Hash函数最好是要选择彼此独立的, 而且是要离散均匀分布的。也就是说Hash函数的结果要等可能(等概率)的落在一个[a, b]区间任的一点(点是非连续的,离散的),如果概率不等,那么在某个bit下标区间内误报的概率就会上升。当然最后,Hash函数还要尽可能的快,虽然是Hash函数时间复杂度都是常数级别,最好是降低下常数吧。

所以适合于做布隆过滤器的Hash函数有:

当然还有其他的。如果你仔细分析,你会发现以上两个系列的Hash函数都不是为密码学设计的,一般来说不是以密码学为目的设计的Hash函数速度会更快。所以MD5SHA-1SHA-2这些算法就不太合适用来做布隆过滤器的Hash函数。

如果你有所怀疑的话,那么可以参考一下这个PR,当把布隆过滤器的实现从MD5切换到murmur时,极大的提高了性能。

当然,也有其他布隆过滤器采用为密码学设计的Hash算法的,这里暂时先不去研究为什么了。

比特向量已经设计为多大?

布隆过滤器的一个优良特性就是可以调整过滤器的误报率。拥有一个大的比特向量的过滤器会拥有比一个相对较小的过滤器有着更低的误报率。

误报率会近似于以下公式:

a1ac93f3gy1g15t747vd0j203p01q3ya

所以你只需要先确定可能插入的数据集的容量大小n, 然后再调整k和m来为你的场景配置过滤器。

所以根据以上公式,你不得不带出来另一个问题,就是K的取值是多少? 也就是说,几个Hash函数才合适?

布隆过滤器使用的Hash函数越多运行速度就会越慢。但是如果Hash函数过少,又会遇到误报率高的问题。所以这个问题上需要认真考虑权衡。

在创建布隆过滤器的时候需要确定k的值,也就是说你需要提前规划n的变动范围。而一旦你这样做了,你依然需要确定m(比特向量长度)和 k (Hash函数的个数)的值。

似乎这是一个十分困难的优化问题,但幸运的是,对于给定的m和n,有一个函数可以帮我们确定最优的k值(公式来自于wiki):

a1ac93f3gy1g15tqjkxm7j209701tglf

当然,有篇论文叫做Building a better bloom filter(Adam Kirsch)已经证明了,其中布隆过滤器只需要2个Hash函数就可以模拟k个Hash函数,并且不会降低精准度。论文中指出以下公式:

Gi(x) = H1(x) +iH2(x).

H1,H2分别是两个互相独立并且离散均匀分布的两个Hash函数,其中 0 <= i <= k - 1 。

当然对于比特向量的索引下标就是最终Hash的结果对向量大小取模就可以了:

index = [hash-value] mod [size of vector]

所以,可以通过以下4个步骤来确定过滤器的大小:

  • 1.确定n的变动范围
  • 2.选定m的值
  • 3.通过公式计算k的最优值(k可以用2个Hash函数的线性组合代替)
  • 4.通过m,n,k计算误报率,如果误报率不在你接受的限度,那么你需要重回步骤2,否则就结束

基本实现

如果用C++模拟比特向量,可以用std::vector<bool>来模拟,一般来说别担心bool在vector里面每个元素是占sizeof(bool)个字节。反而这很可能是被优化了,在bool向量中,bool可能被优化成只占一个Bit 。 当然这具体看实现(implementation defined),一般来说占用空间是被优化掉了。

过滤器的add方法:

// m = size of bit vector
// Gi(x)= H1(x) + i * H2(x)
// index = Gi(x) mod m
uint64_t indexHash(uint8_t i, uint64_t hashA, uint64_t hashB, uint64_t m)
{
    return (hashA + i * hashB) % m;
}

// you need to serialize an object to bytes array
void addObject(const std::vector<uint8_t>& data)
{
    // you need to choose HashA and HashB
    hash_a = HashA(data.data(),data.size());
    hash_b = HashB(data.data(),data.size());
    
    for(int i = 0; i <= k - 1; ++i)
    {
        uint64_t index = indexHash(i, hash_a, hash_b, bit_vector.size()); 
        bit_vector[index] = true;
    }
}

过滤器的contains方法:

// you need to serialize an object to bytes array
bool maybe_contains(const std::vector<uint8_t>& data)
{
    hash_a = HashA(data.data(),data.size());
    hash_b = HashB(data.data(),data.size());

    for(int i = 0; i <= k - 1; ++i)
    {
        uint64_t index = indexHash(i, hash_a, hash_b, bit_vector.size()); 
        if(!bit_vector[index]) return false;
    }

    return true;
}

以上实现的过滤,查找在亿级对象数据量中判断某对象是否存在基本平均响应在纳秒时间。

一般来说,如果不是出什么特别的差错,在大部分场景下误报率几乎可以忍受。

EOF

数据压缩的基本常识


title: 数据压缩的基本常识
date: 2014-03-05 11:41:52
tags:
- 数据压缩

首先解释一个原理:就无损压缩来说,如果一个算法能压缩一个文件,就必然存在另一个文件是压缩之后比原文件还要大的,这个原理还想不通的话可以参考抽屉原理。

于是,设计一个压缩算法,首先就要考虑什么文件是能压缩的,什么文件是不能压缩的。对通用压缩算法来说,我们通常只会做一个基本的假设:在文件中如果出现了一个串Sx,那么其它地方出现Sx的概率相对会比别的串概率要高。

就目前来说,ZIP/RAR这一类算法都是基于这个原理,所以它们也无一例外不能压缩完全随机生成的文件。

于是给出两个命题:

  • 如果你设计的一个压缩算法不是出于对重复串的建模,那么它基本不可能成为通用的压缩算法。
  • 如果你设计的算法〔能压缩任何文件〕,那么一定不存在有效的解压算法。

谈谈函数式编程


title: 谈谈函数式编程
date: 2016-09-05 15:38:56
tags:
- 函数式编程
- lambda演算
- Y组合子

什么是函数式编程

其实有关于函数式编程我有在之前的博文《编程语言为何如此众多》提到过,有兴趣的可以去看看 :)

那么到底什么是函数式呢?听上去好厉害,好高大上的样子。

大家都知道面向对象编程提到的几个特性:封装,继承,多态,一切皆对象。那么其实函数式编程也有它固有的几个特点:不可变量,惰性求值,高阶函数,无副作用,一切皆函数。

从停机问题开始

调程序的时候经常会遇到死循环的Bug,聪明的你有没有想过发明一个自动检查程序里面有没有死循环的工具呢?不管你有没有过这种想法,反正我有过,可惜答案是,没有!

停机问题在wiki上的描述比较学术,又是什么图灵机,又是数学中的集合。因为涉及到计算理论的东西,为了防止小白看不懂,下面用一个小白话来讲,

停机问题:给定任意一个程序及其输入,判断该程序是否能够在有限次计算以内结束。

假设存在停机算法

如果存在停机算法,那么对于给定任意一个函数以及这个函数的输入,停机算法就能告诉你这个函数会不会结束。

function isHalting(func,input){
    return if_func_will_halt_on_input;
}

利用停机判定

设一个函数,并调用它自身:

function foo(func){
    if(isHalting(func,func)){
        while(true);
    }
}

// 判定自身
foo(foo);

这是一个悖论:当函数foo以foo为输入时,到底停机还是不停机?

lambda演算语法

停机问题只是个引子,接下来让我们步入正题。

用形式化的表述,λ演算的语法只有三条:

  • <表达式> ::= <标识符>
  • <表达式> ::= λ <标识符+> . <表达式>
  • <表达式> ::= (<表达式> <表达式>)

例如,根据以上语法,可以写一个加法函数。注意,这个函数是匿名的:

λ x y. x + y

之前定义的3条语法,前二条是用于产生函数,第三条用于函数调用。

; 输出5 
((λx y. x + y) 2 3) 

;为了方便,把匿名函数绑定到一个变量
let add = λ x y. x + y
;输出5
(add 2 3) 

看到这里,其实知道Lisp语言的同学可能就有种似曾相识的感觉了。Lisp语言就是一种函数式编程语言,函数式编程语言就是基于lambda演算发展起来的。细心的人已经发觉,lambda演算与图灵机模型对比,它其实更加侧重于计算的描述,甚至表达式不需要关心函数名,它仅仅是个描述计算过程的计算体。所谓的lambda表达式就是这种计算体的一种叫法,只是在各种编程语言环境下,lambda表达式换了个语法而已。

lambda演算公理

以下是lambda演算的公理系统:

置换公理

  • λ x y. x + y => λ a b. a + b

代入公理

  • (λx y. x + y) a b => a + b

函数生成器

lambda演算相当于一个函数生成器:

let mul = λx y. x*y
let con = λx y. xy

; 代入

mul 3 5  -­‐> 3 * 5 
con 'fu' 'ck' -->  'fuck'

定义IF函数

大家都知道在函数式编程里,一切皆函数,就连什么平时接触到的for,if等语句都不例外。那么在函数式编程里面如何构造一个IF函数呢?

;第一个参数condition为if函数的判断条件,如为真,则执行true_value,反之,false_value

let if = λ condition true_value false_value .
         (condition and true_value) or (not condition and false_value)


;调用if,输出15

if true (mul 3 5) (add 2 3)

=> (true and (mul 3 5)) or (not true and (add 2 3))
=> (mul 3 5) or false 
=> (mul 3 5)
=> 15

;调用if,输出5

if false (mul 3 5) (add 2 3)

=> (false and (mul 3 5)) or (not false and (add 2 3))
=> false or (add 2 3) 
=> (add 2 3)
=> 5
         

递归

来个有意思点的计算,定义一个计算n的阶乘的函数:

let fact = λ n .
          if (n == 0) 1 
                     (mul (n 
                          (fact n - 1)))

问题出现了,我们在定义fact的时候引用的自身(废话,递归不调用自身还叫递归?)。虽然在实际的编译器处理过程中,编译器都可以识别这种定义,但是这不符合严谨的数学公理体系。

如何表达递归

之前的fact函数不是无法引用自身吗?那么我们把“自身”参数化,那么函数内部就可以引用了。

let P = λ self n .
        if ( n == 0) 1 (mul 
                        (n
                         (self n - 1))

然后,再令:

let fact n = P (P n)

; 然后调用,输出24
fact 4
-> P (P 4)
-> if (4 == 0) 1 (mul 4 (P (P n-1)))
-> (mul 4 (P (P 3)))
-> 4 * P (P 3)
-> 4 * 3 * P (P 2)
-> 4 * 3 * 2 * P (P 1)
-> 4 * 3 * 2 * 1
-> 24

可惜,以上还不是真正的递归,只是每次额外多传入了一个参数,反复调用而已。我们的目的是要一个真正的递归函数,但是lambda演算没有这样一个公理可以在定义函数的时候引用自身,怎么办?

Y组合子与不动点

不管之前的说法,我们就认定真正的fact是存在的:

;之前的函数P,为了方便,乘法表示就不用自定义的函数mul了
let P = λ self n .
        if ( n == 0) 1 ( n * self (n - 1))
                         
; 函数P接收2个参数,但是我们可以让函数柯里化(Currying),有时候又称部分求值(Partial Evaluation)
; P接收一个fact,本质上又产生了一个新的单参函数

P (fact) -> λ n .
        if ( n == 0) 1 ( n * fact (n - 1))

注: 函数柯里化本质的意义是把一个多参的函数转换成单参函数作为返回值的形式,这样方便优化,有兴趣可以看知乎的讨论,柯里化对函数式编程有何意义?, 如何理解functional programming里的currying与partial application?

然后,神奇的事发生了,细心的人发现,函数 P (fact) 与之前定义的函数fact相等,

  • P (fact) = fact

我们发现了函数P的一个不动点,什么是不动点呢?就是一个点(广义上的)在一个函数的映射下,函数的值仍然为这个点: f(x) = x 。所以,思路就是找到不动点,如果找到了不动点,就可以把“伪递归”函数P转化为真正的递归函数了。

所以,我们假设需要一个函数Y,它可以找到这个伪递归函数的不动点,即:

  • Y(F) = f = F(Y(F))

其中F(f) = f, 那么就有

  • Y(P) = fact

只要有了Y,就可以把伪递归函数变换成真递归函数了。

构造Y组合子

一起来一睹Y组合子的尊容吧:

  • let Y = λ F. G(G)
  • 其中G = λ self. F(self(self))

验证一下

Y(P)
= G(G) , 其中G = λ self. P(self(self))
= P(G(G))
= λ n. if (n==0) 1 (n * G(G) n - 1)
假设 Y(P) = fact, 那么
Y(P) = fact = λ n. if (n==0) 1 (n * fact n-1)

这就是我们梦寐以求的真正的递归函数!

所以,当我们想定义递归函数的时候,只需要增加一个self参数,按伪递归的方法定义,然后再用Y组合子一套用,就变成我们想要的真递归了。

图灵等价

以上已经成功地推导出了Y组合子,就相当于在λ演算公理体系中推导出了一条定理:

  • 可以在定义函数的过程中引用自身

这条定理是证明λ演算与图灵机等价的一个重要步骤。

那么这两个不同的计算模型等价意为着什么呢?

意味着它的计算能力与我们现实生活中的物理计算机的计算能力是一致的,图灵机的工作模型更接近于物理上的机器,冯诺依曼架构就是图灵机的物理实现。换句话说,也就是说,我们写的任何程序都能用λ演算来描述,同时λ演算描述的函数一定可以由计算机计算。

停机问题的等价问题

回想之前的停机问题,即不可判定一个图灵机在给定任意输入的时候是否可以停机。

这个命题在λ演算中的等价命题是:

不存在一个算法能判定任何两个λ函数是否等价,即对于所有的n,有f(n)=g(n)

现实世界中的函数式编程

之前都是在分析λ演算的理论,是数学化的思维,下面就请看基于λ演算发展出来的函数式语言是什么样子? 用工程化的思维来看看现实世界的函数式编程。

Haskell

Haskell是一种纯函数式编程语言,它追求的是最纯粹的函数式,名字是为了纪念Haskell Curry而命名的。之前提到的Y组合子就是这位老哥发现的,此外他还提出了函数柯里化(Currying),即部分求值。

Haskell中的一切都是函数,甚至没有命令式编程(面向过程或面向对象)中变量的概念。它的变量全部都是只允许一次赋值,然后不可改变,就像数学推导中对变量的赋值一样。

Haskell还没有一般意义上的控制流,如for循环等,取而代之的是递归。

Haskell还有两个重要的特性,即无副作用和惰性求值。

无副作用指的是任何函数在给定同样输入的情况下每次调用的结果都一样,跟数学中的函数是一样的。而惰性求值指的是函数除非需要,否则不会立即计算。

第一个Haskell程序

let max a b = if a>b then a else b


max 3 4   -- print 4

max 1.001 1 -- print 1.001

max "MathxH" "ChenAlex1233"  -- print "ChenAlex1233"

列表

Haskell中的列表定义是这样的:

  • list X :: = [] || elem : (list X)
    即:

  • 空列表 = []

  • [1] = 1:[]

  • [1,2,3] = 1:2:3:[]

输入2:1:3:7:8:[] 可以看到
[2,1,3,7,8]

模式匹配

定义:

  • let first (elem:rest) = elem

输入first [1,3] 可以看到结果是1。
elem:rest 是Haskell的函数参数模式匹配。
对于列表[1,3],实质上是1:3:[], elem匹配了1,rest匹配了3:[], 也就是[3]。

列表求和

accumulate [] = 0
accumulate (elem:rest) = elem + accumulate rest
main = print (accumulate [1,2,3]) -- print 6

判断回文

palindrome [] = True
palindrome [_] = True
palindrome (elem:rest) = (elem == last rest) && (palindrome(init rest)) -- init:返回一个列表中除了最后一个元素的其他元素

palindrome [1,2,3,2,1] -- print True
palindrome [1,1,2]   -- print False
palindrome "madam"  -- print True

删除连续重复元素

cut cond [] = []
cut cond (elem:rest) = if cond elem then 
                         cut cond rest else
                         elem:rest
compress [] = []
compress (elem:rest) = elem : compress (cut (== elem) rest)

compress [1,2,2,2,3,3]  -- print [1,2,3]
compress "aaabbaccc"  -- print "abac"

下面来点华丽的推导过程:

compress [1,2,2,2,3,3]
=> 1 : compress (cut (== 1) [2,2,2,3,3])
=> 1 : compress (if (== 1) 2 then cut (== 1) [2,2,3,3] else [2,2,2,3,3])
=> 1 : compress [2,2,2,3,3]
=> 1 : 2 : compress (cut (== 2) [2,2,3,3])
=> 1 : 2 : compress (if (== 2) 2 then cut (== 2) [2,3,3] else [2,2,3,3])
=> 1 : 2 : compress (cut (== 2) [2,3,3])
=> 1 : 2 : compress (if (== 2) 2 then cut (== 2) [3,3] else [2,3,3])
=> 1 : 2 : compress (cut (== 2) [3,3])
=> 1 : 2 : compress (if (== 2) 3 then cut (== 2) [3] else [3,3])
=> 1 : 2 : compress [3,3]
=> 1 : 2 : 3 : compress (cut (== 3) [3])
=> 1 : 2 : 3 : compress (if (== 3) 3 then cut (== 3) [] else [3])
=> 1 : 2 : 3 : compress (cut (== 3) [])
=> 1 : 2 : 3 : compress []
=> 1 : 2 : 3 : []
=> [1,2,3]

惰性求值

Haskell中可以定义无穷列表,例如:

  • [1..]表示所有的正整数
  • [1,3..]表示所有的奇数

这在大多数编程语言中都是不可思议的,因为大多数语言都是及早求值(early evaluation),Haskell的惰性求值(lazy evaluation)特性可以让列表按需取用。

例如[1,3..] !! 42 可以返回结果85

斐波那契数列

如何用Haskell实现斐波那契数列呢?最符合数学化的描述方法是:

fib 0 = 1
fib 1 = 1
fib a = fib (a - 1) + fib (a - 2)

不巧的是,这个算法是O(2^N)的,因为Haskell编译器还没有聪明到可以实现递归记忆化。

斐波那契数列的线性算法

让我们利用无穷列表来实现线性算法! 一行代码就可以了:

  • fib = 1:1:zipWith (+) fib (tail fib)

fib !! 4是5, fib !! 42 是433494437
fib !! 1000输出:
7033036771142281582183525487718354977018
1269836358732742604905087154537118196933
5797422494945626117334877504492417659910
8818636326545022364710601205337412127386
7339111198139373125598767690091902245245
323403501

解释下以上代码:

tail返回列表除了第一项以外的后面内容,例如 tail [1..] 返回 [2..]

zipWith是将两个列表的每个元素通过一个函数非别计算,并返回结果的列表,例如:

  • zipWith (*) [2,3,5] [1,2,3] 返回[2,6,15]

于是乎,fib = 1:1:zipWith (+) fib (tail fib) 生成了一个无穷列表,前面两个元素都是1,后面的元素由现有列表错位相加而成,即:

[1,  1,  2,  3,  5,  8,  13,  21…]   //fib    

+ [1, 2, 3, 5, 8, 13, 21, 34…] //tail fib
= [2, 3, 5, 8, 13, 21, 34, 55…]

由于惰性求值,列表不会被立即计算,只有当我们用到其中元素的时候才会算。

快速排序

qsort (elem:rest) = (qsort lesser) ++ [elem] ++ (qsort greater)
   where
    lesser = filter (< elem) rest
    greater = filter (>= elem) rest

++ 用于列表连接
filter返回列表中满足条件的元素组成的列表

二叉树并表示

data Tree a = Empty | Node a (Tree a) (Tree a)
tree = Node 'd'
    (Node 'b'
       (Node 'a' Empty Empty)
       (Node 'c' Empty Empty)
    )
    (Node 'e'
      Empty
      (Node 'g'
        (Node 'f' Empty Empty)
        Empty
      )
    )

以上代码构建了一个二叉树,是这样的:

       d
      / \
     b   e
    / \   \
   a   c   g
          /
         f

下面对这个二叉树进行中序遍历:

inorder  Empty  =  [] 
inorder  (Node  value  left  right)  = 
inorder  left  ++  [value]  ++  inorder  right 

inorder tree  -- print "abcdefg"

然后求树的高度:

height  Empty  =  0 
height  (Node  value  left  right)  =  
max  (height  left)  (height  right)  +  1 

height tree -- print 4

高阶函数

高阶函数的参数是函数,通过部分求值可以返回函数。

traverse  func  zero  Empty  =  zero 
traverse  func  zero  (Node  value  left  right)  = 
func  value  
(traverse  func  zero  left)  
(traverse  func  zero  right) 
height_func  _  a  b  =  max  a  b  +  1 

traverse  height_func  0  tree  -- print 4

中序遍历函数:

inorder_func value left right = left ++
[value] ++ right

通过部分求值产生低阶函数:

inorder = traverse inorder_func []

inorder tree -- print "abcdefg"

闭包

function  make_closure()  { 
  var  inner_varible  =  0; 
  return  function  ()  { 
    return  inner_varible++; 
  } 
} 
var  counter  =  make_closure(); 
counter();  //  0 
counter();  //  1

用闭包实现柯里化

多数编程语言无法部分求值,原因是柯里化与参数表机制冲突。但可以用闭包实现。但是Java,C++ 11等主流编程语言对于闭包的支持就是半残。下面用js来表达吧。

function pow(x, y){
    return x ^ y;
}


// 对pow函数部分求值
function  pow5(x)  { 
  return  pow(x,  5); 
} 
pow5(2);  //输出  32

上面的代码实质上是:

function  pow5(x)  { 
  return  function (x, 5){
    return x ^ 5;
};  
} 

EOF

C++11之函数式风格编程


title: C++11之函数式风格编程
date: 2017-03-3 10:48:40
tags:

  • C/C++
  • 函数式编程

C++是多范式编程语言,程序员可以选择多重范式的结合来编程,比如面向对象,面向过程,泛型编程,函数式特性。尤其是C++ 11标准中完善了函数式的一些特性,比如,lambda表达式,变参模版,新的STL函数bind。下面就讲讲怎样用C++ 11来写函数式风格的代码。

函数式风格编程

  • 自动类型推导,auto和decltype

  • lambda表达式的支持, 闭包,函数即数据

  • 偏函数应用(partial funtion application),std::function和std::bind,lambda表达式和auto

  • STL与高阶函数的组合

  • 用变参模版(variadic templates)来进行列表生成(List manipulation)

  • 用模版全特化(full template
    specialisation)和偏特化(partial template
    specialisation)来进行模式匹配(pattern matching)

  • 用std::async来进行惰性求值

为什么要以函数式风格来编程

STL结合lambda表达式更加简洁高效

std::accumulate(std::begin(vec),std::end(vec),                            
         [](int a,int b){return a+b;});

模式匹配之模版元编程

// 编译时计算阶乘
template <int N>
struct Fac{ static int const val= N * Fac<N-1>::val; };
template <>
struct Fac<0>{ static int const val= 1; };

还有更好更简洁的编程风格

// 使用range for
for (auto v: vec) std::cout << v << std::endl;

什么是函数式编程

  • 函数式编程就是以数学中的函数概念进行编程

  • 数学中的函数在给定同样参数的情况下,每次调用函数的返回值都相同,不会有副作用(side-effect)

  • 正因为函数调用没有副作用,那么函数从语义上相当于结果(Result),是等价的,可以互相替换

  • 因为无副作用的特性,编译器的优化器可以从更高层次来优化,增加优化潜力,重新组合函数的调用顺序,或者把不同的函数调用放到不同的线程中并行计算,提高并发可能性

  • 程序的控制流就是以数据依赖驱动的了,而不是指令顺序

函数式编程的特点

  • 惰性求值(Lazy evaluation)
  • 一等函数(first-class function)
  • 高阶函数(higher-order function)
  • 纯函数(pure function) (这个C++不是纯函数,只有Haskell是纯函数)
  • 递归
  • 列表生成(manipulation of list)

一等函数

  • 一等函数属于一等公民,与数据地位相同,函数即是数据
  • 函数可以当作参数来传递
  • 函数可以当作值一样被其他函数返回
  • 函数能被存储在变量或者数据结构中

其实C语言也有类似的特性,函数指针,这样就有更强的表现能力,也明面上支持了回调。C++ 11直接支持lambda表达式了。就不需要那么复杂和不安全的函数指针了。

一等函数表达能力之分发表(Dispatch Table)

std::map<const char,std::function<double(double,double)>>  tab;

tab.insert(std::make_pair('+',[](double a,double b){return a + b;}));
tab.insert(std::make_pair('-',[](double a,double b){return a - b;}));
tab.insert(std::make_pair('*',[](double a,double b){return a * b;}));
tab.insert(std::make_pair('/',[](double a,double b){return a / b;}));

std::cout << "3.5+4.5= " << tab['+'](3.5,4.5) << std::endl;  //8
std::cout << "3.5*4.5= " << tab['*'](3.5,4.5) << std::endl;  // 15.75

tab.insert(std::make_pair('^',                                        
                  [](double a,double b){return pow(a,b);}));

std::cout << "3.5^4.5= " << tab['^'](3.5,4.5) << std::endl;  // 280.741

高阶函数

  • 高阶函数是一种接受其他函数作为参数或者是返回其他函数的一种函数

说到这里,可能有人懵了,一等函数和高阶函数有啥不一样?请看以下代码块就知道了:

function make_add(){
    var f = function (a, b){ return a + b;}; 
    return f;
}

以上代码,函数make_add就是高阶函数,匿名函数function(a,b)可以被bind到变量f上,或者本身也可以直接返回,所以这匿名函数就是一等函数。

所以,高阶函数用朴素的观点来理解的话,可以这么说:高阶函数是一等函数的应用

很多函数式编程语言,有三种经典的高阶函数应用直接体现:

  • map , 应用一个函数到列表的每一个元素(element)上

  • filter, 从一个列表中移除一些元素

  • fold, 通过二分操作分解一个列表,直到一个列表变成单值(a single value)

很多编程语言已经有以上三种语义的支持了:

这三种语义有无数经典的应用场景:

Map + Reduce = MapReduce

Haskell

vec = [1 . . 9]
str = ["Programming","in","a","functional","style."]

map(\a  a^2) vec    -- [1,4,9,16,25,36,49,64,81]
map(\a -> length a) str -- [11,2,1,10,6]

filter(\x-> x<3 || x>8) vec  -- [1,2,9] 
filter(\x  isUpper(head x)) str -- [“Programming”]

foldl (\a b  a * b) 1 vec   -- 362800
foldl (\a b  a ++ ":" ++ b ) "" str -- “:Programming:in:a:functional:style.”

Python

vec = range(1,10)
str = ["Programming","in","a","functional","style."]

map(lambda x :  x*x , vec) # [1,4,9,16,25,36,49,64,81]
map(lambda x : len(x), str) # [11,2,1,10,6]

filter(lambda x: x<3 or x>8 , vec) # [1,2,9]
filter(lambda x: x[0].isupper(),str) # [“Programming”]

reduce(lambda a , b: a * b, vec, 1) # 362800
reduce(lambda a, b: a + b, str,"") # “:Programming:in:a:functional:style.”

C++ 11

std::vector<int> vec{1,2,3,4,5,6,7,8,9}
std::vector<std::string> str{"Programming","in","a","functional",       
                "style."}

std::transform(vec.begin(),vec.end(),vec.begin(),                 
        [](int i){ return i*i; }); // [1,4,9,16,25,36,49,64,81]
std::transform(str.begin(),str.end(),back_inserter(vec2),         
        [](std::string s){ return s.length(); }); // [11,2,1,10,6]

// [1,2,9]
vec.erase(std::remove_if(vec.begin(),vec.end(),
[](int i){ return !((i < 3) or (i > 8)) }),vec.end());
// [“Programming”]
str.erase(std::remove_if(str.begin(),str.end(),                   
        [](string s){ return !(isupper(s[0])); }),str.end());

std::accumulate(vec.begin(),vec.end(),1,                          
         [](int a, int b){ return a*b; }); // 362800

//“:Programming:in:a:functional:style.” 
std::accumulate(str.begin(),str.end(),string(""),                 
         [](string a,string b){ return a+":"+b; });      

纯函数

  • 纯函数是独立的,这样让程序验证更加容易,也更加利于重构,测试,维护

  • 非常有利于优化,比如,保存函数的调用结果,并行化

  • 单子(Monad),是Haskell处理“不纯世界”的一种解决方案,它封装了不纯的世界,也是Haskell中的一个很重要的子系统,它也是表示计算的一种结构,甚至可以定义一组计算的组成结构

递归

C++ 11

// 编译时计算阶乘
template <int N>
struct Fac{ static int const val= N * Fac<N-1>::val; };
template <>
struct Fac<0>{ static int const val= 1; };

// 计算过程
Fac<3>::value = 3 * Fac<2>::value
              = 3 * 2 * Fac<1>::value
              = 3 * 2 * 1 * Fac<0>::value
              = 3 * 2 * 1 * 1
              = 6
  • 递归是一种控制结构
  • 递归和列表处理结合在函数式语言中是一种强力的模式

同样的计算阶乘方式,我们用Haskell的模式匹配来实现以下:

fac 0 = 1
fac n = n * fac (n-1)

列表处理

  • 处理列表头head(x)
  • 递归处理到列表尾tail(xs)

比如:[1,2,3,4,5] head(x) = [1] tail(xs) = [2,3,4,5]

那么用模式匹配+递归写出如下Haskell代码:

-- 求和
mySum []     = 0
mySum (x:xs) = x + mySum xs
mySum [1,2,3,4,5]    -- 15

-- 计算过程
mySum [1,2,3,4,5] = 1 + mySum [2,3,4,5]
                  = 1 + 2 mySum [3,4,5]
                  = 1 + 2 + 3 mySum [4, 5]
                  = 1 + 2 + 3 + 4 mySum [5]
                  = 1 + 2 + 3 + 4 + 5 + mySum []
                  = 1 + 2 + 3 + 4 + 5 + 0
                  = 15

-- 实现map语义
myMap f [] = []
myMap f (x:xs)= f x: myMap f xs  
myMap (\x  x*x)[1,2,3] -- [1,4,9]

-- 计算过程
myMap (\x  x*x)[1,2,3] 
=> (\x -> x*x) 1 : myMap (\x -> x*x) [2,3]
=> 1 : (\x -> x*x) 2 : myMap (\x -> x*x) [3]
=> 1 : 4 : (\x -> x*x) 3 : myMap (\x -> x*x) []
=> 1 : 4 : 9 : []
=> [1,4,9]

  

下面用C++ 11 的变参模版来做以上的求和:

template<int ...> struct mySum;
template<>struct
mySum<>{
static const int value = 0;
};

template<int head, int ... tail> struct
mySum<head,tail...>{
static const int value= head + mySum<tail...>::value;
};

int sum = mySum<1,2,3,4,5>::value;  // 15

//计算过程
mySum<1,2,3,4,5>::value
=> 1 + mySum<2,3,4,5>::value
=> 1 + 2 + mySum<3,4,5>::value
=> 1 + 2 + 3 + mySum<4,5>::value
=> 1 + 2 + 3 + 4 + mySum<5>::value
=> 1 + 2 + 3 + 4 + 5 + mySum<>::value
=> 1 + 2 + 3 + 4 + 5 + 0
=> 15

其实呢,归而结网,列表处理的核心**就是模式匹配。

再举个例子,用Haskell的模式匹配写一个乘法函数:

mult n 0 = 0
mult n m = (mult n (m  1)) + n

-- 6
mult 3 2

--计算过程
mult 3 2
=> (mult 3 (2 - 1)) + 3
=> (mult 3 1) + 3
=> (mult 3 (1 - 1)) + 3 + 3
=> (mult 3 0) + 3 + 3
=> 0 + 3 + 3
=> 6

用C++ 11的模版元编程来写乘法:

template < int N1, int N2 > 
class Mult { 
  static const int value =  Mult<N1, N2 - 1>::value + N1; 
};

template < int N1 > 
class Mult <N1,0> { static const int value = 0; };

// 6
int result = Mult<3,2>::value;

//计算过程
Mult<3,2>::value
=> Mult<3,1>::value + 3
=> Mult<3,0>::value + 3 + 3
=> 0 + 3 + 3
=> 6

惰性求值

  • 简单说,就是到需要的时候再计算,不需要的时候不必计算
  • 优势是,节省计算时间,节省内存使用。还有可以和无限数据结构结合(infinite data structures)

EOF

为什么使用多线程在大多数情况下是个坏注意?


title: 为什么使用多线程在大多数情况下是个坏注意?
date: 2018-03-20 14:00:00
tags:

  • 线程

简介

什么是线程?

  • 诞生并成长于操作系统这个世界中
  • 在用户级别的工具中慢慢演进
  • 被提议为多种多样的问题的一种解决方案

那么,上面说的如此明确了,我们是否在现实工作中成为一个彻头彻尾的“多线程程序员”呢?

多线程带来的问题: 多线程程序很难编写

相对于多线程的一个折中方案: 采用事件驱动的**

声明:

  • 对于大多数的场景,事件要比多线程更好
  • 多线程一般只能用于真正确实需要用到CPU并行的场景

多线程的使用场景

  • 操作系统: 一个内核线程对应一个用户进程

  • 科学计算: 一个线程可以绑定到一个CPU上执行,有效利用多核,提高性能。(比如大规模非线性方程组运算等等)

  • 分布式系统: 大量进程的并发请求(IO复用)

  • GUI编程: 线程对应用户的Action(按钮点击等)。比如需要一个后台线程长时间做运算,并最终显示结果在GUI上。

  • 多媒体,动画: 类似于GUI

为什么多线程很难

  • 同步:访问共享数据的时候都要加锁。忘记加锁了?对不起,数据损毁。

  • 死锁: 多线程访问多个锁,锁之间有循环依赖。多个进程互相等待,造成系统挂起

  • 难以Debug: 数据依赖,操作时序依赖

  • 多线程破坏了抽象: 不能设计独立强的模块

  • 回调函数不能与锁一起良好进行工作

  • 很难达到理想性能:锁的粒度控制不好容易造成并发度降低,操作系统的调度和上下文切换机制限制了软件性能(用户态切换到内核态的时间是很昂贵的)

  • 多线程不被良好支持: 操作系统提供的原生线程API不兼容,难以移植。

事件驱动编程

  • 单个执行流: 没有CPU并行
  • 可以注册感兴趣的事件(通过回调函数)
  • Event-loop等待事件,并调用对应的event handler
  • event handler之间没有优先级(内核线程调度有优先级)
  • event handler的生命周期很短 (内核线程创建销毁耗时)

事件驱动编程被用在哪些场景

  • 大多数的GUI框架,按钮响应等对应一个Event,一个Event对应一个Event Handler。一个Event Handler可以实现一些行为,打开文件,删除等等

  • 分布式系统。一个handler对应每个输入源(Socket),handler处理请求和回送响应。事件驱动型的IO复用。

事件驱动带来的问题

  • handler如果进行长时间的运算,会导致程序长时间无响应(因为是单线程执行流,比如node.js)。对于这种耗时的运算,可以fork一个子进程来处理,子进程通过事件来通知主进程自己的完成情况。

  • 不能跨事件的维护一个local state。(handler必须返回)

  • 单线程执行流不能利用多核,也就是不适用于科学计算软件

事件VS多线程

  • 事件尽可能避免了并发,多线程相反。事件驱动更好入门,没有数据竞争和死锁。多线程可能对于一个简单场景用起来都更复杂。

  • 事件驱动的程序更好Debug。时序依赖只跟事件有关,没有内部调度。

  • 事件驱动在单核CPU上性能往往比多线程的程序性能更好。

  • 事件驱动的程序比多线程程序更好移植。

  • 多线程程序才能真正利用多核

你应该放弃多线程吗

  • 不能,在高性能服务器上必须有效利用多线程,比如数据库软件。

记一次驱动开发中的蓝屏死机问题


title: 记一次驱动开发中的蓝屏死机问题
date: 2014-03-05 13:37:00
tags:

  • windows
  • 驱动开发

windows驱动开发中,感觉很多小问题都会导致一些比较麻烦的错误,很多不良习惯都可能会导致系统崩溃。原因是对内核原理的理解太欠缺了,因为驱动运行在RING0优先级,开发的时候必须相当注意细节,不然调试的时候会很麻烦(有些时候Dump文件分析出来是错误的)。原来我一直奇怪写驱动程序问什么要把有些例程放到非分页内存中,今天算是豁然开朗。只怪原来看内核原理的时候没太仔细。具体原因是这样的:因为windows的缺页中断处理程序是运行在DISPATCH_LEVEL的级别的。而我的驱动某些例程一般是等于DISPATCH_LEVEL这个中断级别。也就是说。。如果我把我的代码放到分页内存中。。那操作系统很可能在某个时候就把它从物理内存换出到磁盘页面交换文件中了。。然后EIP指针如果访问到的下一个内存页在换页文件中。。那操作系统将产生缺页中断。。然后这缺页中断的级别与我的代码例程是一个级别的。。所以不能被打断。。EIP就会访问的一个错误的内存地址(很可能读/写到了其他进程,也可能是系统中重要的数据)上,,用户层的程序内存访问违例的异常操作系统能捕捉到。反馈给用户。。。而在驱动内核层内存访问错误。。这是一个很严重的问题。。操作系统发现立即就以蓝屏的方式处理了。。严重的还导致系统崩溃。就悲剧了。。。另外还有一点就是OS的线程调度程序也运行在这个级别。。这也应该有意识。。不然引起线程切换导致蓝屏也时常发生。。。。。。。。

TIPS:

优先级:
DISPATCH > APC > PASSIVE

IRQL是Interrupt ReQuest Level,中断请求级别。一个由windows虚拟出来的概念,划分在windows下中断的优先级,这里中断包括了硬中断和软中断,硬中断是由硬件产生,而软中断则是完全虚拟出来的。

处理器在一个IRQL上执行线程代码。IRQL是帮助决定线程如何被中断的。在同一处理器上,线程只能被更高级别IRQL的线程能中断。每个处理器都有自己的中断IRQL。
我们在调用NDIS API时,在DDK帮助文档中都有该API函数的所在级别。

PASSIVE_LEVEL

IRQL最低级别,没有被屏蔽的中断,在这个级别上,线程执行用户模式,可以访问分页内存。

APC_LEVEL

在这个级别上,只有APC级别的中断被屏蔽,可以访问分页内存。当有APC发生时,处理器提升到APC级别,这样,就屏蔽掉其它APC,为了和APC执行一些同步,驱动程序可以手动提升到这个级别。比如,如果提升到这个级别,APC就不能调用。在这个级别,APC被禁止了,导致禁止一些I/O完成APC,所以有一些API不能调用。

DISPATCH_LEVEL

这个级别,DPC 和更低的中断被屏蔽,不能访问分页内存,所有的被访问的内存不能分页。因为只能处理非分页内存,所以在这个级别,能够访问的Api大大减少。

防止数据被恢复的可能


title: 防止数据被恢复的可能
date: 2016-09-02 18:17:39
tags:
- 信息安全
- 数据恢复

平时大家有没有好奇过,为什么硬盘上被删除的数据有时候还能被还原恢复呢?其中的原理是什么呢?

其实,如果操作系统学得不错的人应该知道,数据文件被删除只是操作系统把文件从文件系统中删除,也就是文件在文件系统中的索引(地址)被删除,意思就是,操作系统告诉文件系统:Hey,哥们儿,我不需要这个文件了,你把这文件从检索索引中删除吧,我不再需要了。其实真实的文件数据还会真实存在物理硬盘上,没有真正被消除,除非有新的文件数据被保存并恰好覆盖了之前的文件数据。数据恢复就是依赖这个原理工作的。

那么如何防止数据被恢复的可能呢?其实如果脑袋聪明的人一下子就能想到,我往原文件里面大量反复写入Dirty Data来覆盖之前的数据不就可以了么?答对,就是这个思路。其实现今无论多么先进的数据销毁软件都是基于这么个思路(甚至美国**情报局销毁绝密数据也是这么干的)。只不过越要降低被恢复的可能性写入的次数随之增加,所以耗费的时间也越多。

垃圾数据只写入一次还不够,需要多次覆盖,这是因为,直观上理解,一次写入后,磁盘上的数据就变化了。其实在硬件层面,不是这样的,还是能够恢复的。恢复的技术有下面两种:

  • 第一种是:当硬盘读写头在硬盘上写入一位数据时,它使用的信号强度只是用来写入一位数据,并不会影响到相邻位的数据。由于这个写入信号并不是很强,因此你可以通过它写入的数据位的绝对信号强度来判断此前该数据位所保存的是何种数据。换句话说,如果二进制数据0被二进制数据1所覆盖,其信号强度会比二进制数据1被覆盖要弱一些。使用专门的硬件就可以检测出准确的信号强度。把被覆盖区域读出的信号减去所覆盖数据的标准信号强度,就能获得原先数据的一个副本。更令人吃惊的是,这一恢复过程可以被重复7次。因此如果想避免别人使用这种方法来窃取你的数据,至少要覆盖该数据区域7次,而且每次还应该使用不同的随机数据。

  • 第二种数据恢复技术则是利用了硬盘读写头的另一个特点:读写头每次进行写操作的位置并不一定对得十分精确。这就能让专家们在磁道的边缘侦测到原有的数据(也被称为影子数据)。只有重复地覆写数据才能消除这些磁道边缘的影子数据。由于硬件的这种特性,在销毁文件的时候,才会需要多次覆盖。

所以根据以上的特性,才有了许多数据擦除算法的诞生,覆盖次数越多,随机垃圾数据越“随机”,被恢复的可能性也就越低,安全性也就越高。到了Gutman method这样的级别,理论上已经证明了,经过此算法处理过的数据,没可能恢复。 但是,弱点就是速度很慢,如果需要有大量机密数据要被销毁,这将是耗时的任务。

我写过一个数据擦除的库,等级最高也就是支持到Gutman这个级别。其中DOD5220_22M算法是之前美国**情报局采用的,不过现在被抛弃了,我猜测原因就是速度和安全性的权衡。至于他们为什么没有采用Gutman算法,其实也是速度和安全性的权衡,因为太慢了:) 这告诉我们,评价衡量一个工具需要综合考量。

从群环域到椭圆曲线密码学

从群环域到椭圆曲线密码学

干了区块链这行当,快一年了,还没有写过有关于区块链相关的密码学呢,之前是没有仔细研究这块内容,再不自己写写总结,总有点对不住自己。如果以后不做这行了,哪敢跟别人说我曾经做过区块链啊

从集合到群

首先还是从集合说起吧。

最早的时候,我记得我们应该是在初中的时候就引入集合的概念了。我们知道很多种关于数字的集合(以下字母均为大写字母):

  • N表示所有自然数集合 {1,2,3,4,5 ...}
  • Z是所有整数的集合 {... -3, -2, -1, 0, 1, 2, 3 ...}
  • Q是所有有理数的集合 (有理数可以写为两个整数的比 a/b 其中a,b为整数,且b不等于0)
  • R是所有实数的集合 (实数是数轴上所有有理数和无理数的总称)
  • C是所有复数的集合 (复数是复平面上的所有点 a + b*i 其中a,b为实数)

从小学到高中,我们都直接或间接地一直在学习数字的集合,学习加减乘除等运算。换个角度,如果我们把这些运算引入集合会不会发现有趣的性质呢?大家有没有想过?

设集合G,然后我们定义有一个*这个二元运算(注意,这个运算不一定是乘法,也不一定是加法,它表示一个二元运算,仅此而已)。对于集合的任意元素a,b,都有以下关系成立:

  • a * b ∈ G

此时,我们称: 关于运算*, 集合G封闭。 (封闭有些时候也称闭包closure)

从集合出发,加入二元运算,从而定义了群的概念,对于某二元运算*, 单单具有封闭性的集合叫原群(magma)。

如果该运算还具有结合律,也就是不拘泥于运算的顺序:

  • (a * b) * c = a * (b * c) ∈ G 且 a, b , c ∈ G 且 a,b,c为G中的任意三个不同的元素

如果对于运算 * ,具有封闭性,结合律的集合称为半群(semi-group)。

如果对于运算 *, 满足以下性质:

  • a * e = e * a = a 且 a, e ∈ G 且 a为G中的任意一个元素

此时我们称元素e是在集合G下运算*中的单位元。

举个例子, 对于集合Z,在运算 + (加法)里单位元就是0,但是在运算 × (乘法)里单位元是1。

如果对于运算 *, 具有封闭性,结合律和单位元的集合称为幺半群(monoid)。哈哈,看到这里,如果对于Haskell等函数式语言比较熟悉的人是不是经常听过单子(monad)这种概念?他们确实是有关的,这里不打算延伸,感兴趣请看《我所理解的monad(1):半群(semigroup)与幺半群(monoid)》

如果对于运算 *, 满足以下性质:

  • a * b = b * a = e 且 a, b, e ∈ G

此时我们称元素b是关于运算*的a的逆元。

举个例子,对于集合R,关于运算 + (加法)里,4的逆元就是-4,但是在运算 × (乘法)里4的逆元是1/4。

这么一来,如果对于运算 *, 具有封闭性,结合律,单位元,逆元就称为群(group)。

所以引出了群的定义(公理),将满足以下性质的集合G称为群:

  • 关于运算 * 封闭
  • 对于任意的元,都满足结合律
  • 存在单位元
  • 对于任意的元,都有与其对应的逆元

阿贝尔群

阿贝尔群是群中的特殊存在,它对群的概念又加上了特殊的限制,也就是在集合G上,对于运算*, 任意元都满足交换律,那么就称为阿贝尔群:

  • a * b = b * a 且 a, b ∈ G 且 a,b为G中的任意两个元素

阿贝尔群与普通群的区别就在于是否满足交换律。阿贝尔群的公理如下:

  • 关于运算 * 封闭
  • 对于任意的元,都满足结合律
  • 存在单位元
  • 对于任意的元,都有与其对应的逆元
  • 对于任意的元,都满足交换律

当然,阿贝尔群又称为交换群,纯数学专业可能会有开设《交换群论》这样的深入点的专业课。

从群到环

环(Ring)又在阿贝尔群的基础上,加入了另一种二元运算,暂且这里叫做乘法二元运算吧,这个乘法与初等代数不大一样。加法运算也与初等代数不大一样。

所以环就是二种不同的二元运算作用在集合G上,并满足以下性质:

  1. 关于运算 + (加法):
  • 封闭
  • 存在单位元
  • 所有元素都满足结合律
  • 所有元素都满足交换律
  • 任意一个元素都存在与其对应的逆元
  1. 关于运算 × (乘法):
  • 封闭
  • 存在单位元
  • 所有元素都满足结合律
  1. 关于运算 + 和 × :
  • 所有元素都满足分配律, 即(a + b) × c = (a × c) + (b × c)

如果细心的人仔细看环的公理,并对比之前阿贝尔群的公理,其实会发现:

  • 环关于加法一定构成了阿贝尔群
  • 换关于乘法不一定是阿贝尔群(也有可能是阿贝尔群)
  • 环关于乘法一定构成幺半群

交换环

交换环就是环关于乘法运算上,所有元素满足交换律:

  1. 关于运算 + (加法):
  • 封闭
  • 存在单位元(0)
  • 所有元素都满足结合律
  • 所有元素都满足交换律
  • 任意一个元素都存在与其对应的逆元
  1. 关于运算 × (乘法):
  • 封闭
  • 存在单位元(1)
  • 所有元素都满足结合律
  • 所有元素都满足交换律
  1. 关于运算 + 和 × :
  • 所有元素都满足分配律, 即(a + b) × c = (a × c) + (b × c)

剩余类环

我们举个例子,整数集合Z关于加法和乘法构成环,我们把这个环称为整数环,如果我们把mod运算考虑进加法和乘法运算的话,那么就会变成这样(设m为某个常数):

  • a + b mod m
  • a × b mod m
  • (a + b) × c mod m

那么这些运算的结果集合也构成了环: Z/mZ {0,1,2,3,4 ... m - 1}。这个环我们称为剩余类环,所以可以把Z和Z/mZ同等看待。

这两种环都满足换的公理,但是,这两种环还是不大相同,Z就像数轴上排列的点,Z/mZ类似于时钟的表盘构成的圆环; Z是无限集合, Z/mZ是有限的集合; Z具有无限性,Z/mZ具体周期性,集合大小被限制在了某个范围。

只要从环中推导出某个定理,那么这个定理一定适用与Z,也同样适用于Z/mZ。这就是抽象代数的魅力,哈哈,这样是不是mod运算有点类似于化简,归整? 把无限变成有限,将无限宇宙尽收掌心,好利于研究或计算啊。这时候我想起了霍金写的《果壳中的宇宙》。

从环到域

从环(特指交换环)的公理来看,似乎关于乘法运算上与加法运算对比还缺少一个特性: 关于乘法的逆元。 也就是说,环里不一定能进行除法运算,除法运算本质上就是乘法逆元。

说到这里就可以看到,如果一个环关于乘法上,每个非零的元素都要有乘法逆元(非零是因为0不能作除数),那么就可以把这个环称为域(Field)。

哈哈,我们终于从群开始慢慢过渡到了域。

现在来回顾一下,对于群而言,集合中只能定义一种二元运算,对环来说,定义了两种二元运算,而对于域是定义了三种二元运算吗? 不是的。

如果你仔细想想加法和乘法的相反运算都是通过逆元来定义的,那么你就清楚了。存在加法和关于加法上的逆元就能进行减法运算,同理,存在乘法和关于乘法上的逆元(除0以外)就能进行除法运算。所以关于乘法是否存在逆元就是环(交换环)和域的唯一区别。

域的公理自然而然就是这样了:

  1. 关于运算 + (加法):
  • 封闭
  • 存在单位元(0)
  • 所有元素都满足结合律
  • 所有元素都满足交换律
  • 任意一个元素都存在与其对应的逆元
  1. 关于运算 × (乘法):
  • 封闭
  • 存在单位元(1)
  • 所有元素都满足结合律
  • 所有元素都满足交换律
  • 除0以外的所有元素都存在与其对应的逆元
  1. 关于运算 + 和 × :
  • 所有元素都满足分配律, 即(a + b) × c = (a × c) + (b × c)

由此可见,域是一种能够进行加减乘除的代数结构,是集合与四则运算的推广。当然,还有一种叫格(lattice)的代数结构,它目前被用在了抗量子算法的密码学领域(基于格的密码学),还有如果你涉及程序语言理论方面的研究,也会用到格的概念。这里不打算深究,因为文章后面的话题暂时似乎是用不到格的概念。

回顾下之前我们提到的整数环Z,整数集合Z是可以构成环的,但是整数环不能构成整数域(逆元不属于整数集合),但是整数环加入除法(乘法逆元),能够构成有理数域。

考虑整数集合Z {... -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5}, 它构成一个整数环Z,我们回到剩余类环的概念,既然整数环是无限的,那么我们就来研究有限的剩余类环吧,反正是一样的,它也是环,对吧。剩余类环Z/mZ是对运算结果加入了mod操作。那我们就来测试一下(设m为5):

那么剩余类环Z/mZ {0, 1, 2, 3, 4}

3 + 4 mod 5 = 2

  • 4 + 1 mod 5 = 0 (对于加法,1的逆元是4)
  • 1 + 4 mod 5 = 0
  • 3 + 2 mod 5 = 0 (对于加法,3的逆元为2)
  • 2 + 3 mod 5 = 0
  • 0 + 0 mod 5 = 0 (0的加法逆元还是它本身)

1 × 3 mod 5 = 3

  • 3 × 2 mod 5 = 1 (对于乘法,3的逆元是2)
  • 2 × 3 mod 5 = 1
  • 4 × 4 mod 5 = 1 (对于乘法,4的逆元是4)
  • 1 × 1 mod 5 = 1 (对于乘法,1的逆元是1)

这里例子中为什么没有零?注意,之前提到过,除0以外的任何元素才有乘法逆元,这样才是域。这点要一定记住。

因为 (3 + 4) × 2 = 3 × 2 + 4 × 2 满足分配律

(3 + 4 mod 5)× 2 mod 5 = 4 mod 5 = 4

3 × 2 mod 5 + 4 × 2 mod 5 = 1 + 3 = 4

(3 + 4) × 2 = 3 × 2 + 4 × 2 = 14 mod 5 = 4

经过以上运算的结果得知,在m = 5的时候,剩余类环Z/mZ是域,因为满足域公理。

那么这样说来,对于剩余类环Z/mZ就一定是域吗?不是的,不信你可以用m = 6来做运算看看,最后结果就不是域。慢慢经过你的m测试越来越多,你会发现,居然要m是质数(素数)的情况下,剩余类环Z/mZ才是域。m = 质数的情况下,剩余类环这个域也被称为有限域。记作:

设p是质数,m = p

  • Fp = Z/pZ

如果把剩余类环看作是整数的微缩模型,那么用质数p构造出的有限域Fp也可以说是有理数的微缩模型吧。这个有理数域的微缩模型被限定在了p内,并且有限域都是整数! 能够化简到如此简单,数学真的很美妙,此时我的眼眶是湿润的。

这时候你就可以想象下,在平滑无限细分的有理数域把它化简到有限域Fp,那么这个离散化的过程最终的成果就是,计算机就可以处理有限并且离散的数据了,因为最开始,计算机是不能精确求光滑无限细分的问题的

有限域,这个概念,慢慢会出现在椭圆曲线密码学中。接下来我们就开始接触椭圆曲线密码学吧。

椭圆曲线密码学

椭圆曲线

其实椭圆曲线是非欧几何下引伸出来的一种几何。大家都知道在欧式几何里面平面上的两条平行线不相交吧?如果修改欧几里德第五公设(过直线外一点,有且仅有一条与该直线的平行线),那么就可以导出了非欧几何。非欧几何大概有两种:

  • 罗氏几何(双曲几何)(修改后的第五公设:过直线外一点至少有两条不同的直线和已知直线平行)
  • 黎曼几何(椭圆几何) (修改后的第五公设:过直线外一点所作任何直线都与该直线相交,也就是没有任何一条平行线)

在非欧几何的研究中,可以导出很多更加有趣的几何性质。

比如:

  • 罗氏几何中,在马鞍面(双曲抛物面)上画一个三角形,其内角和就小于180度。
  • 黎曼几何中,在球面上画一个三角形,其内角和就大于180度。

理解了黎曼几何,那么就理解了黎曼面上看似平行的平行线其实是会有交点的。可以理解为相交在很远很远的地方,即定义了平行线相交于无穷远点(point at infinity)。

好了,这样我们假设在欧几里德平面上画一条直线A,我们想像一下,我们为直线A添加一个无穷远点P,那么我们得到一个扩大的直线(直线A的有穷点和无穷远点P构成),这个直线专业点的叫法叫射影直线(projective line),也称一维射影空间。然后我们在该平面上画一条直线B,它平行于A,我们假设这条线B与A相交于无穷远点P。(因为欧几里德平面的平行线是不可能有交点的,所以只能用无限的思维假设AB两条平行线在无穷远处相交于点P)

这样我们试着导出了以下三个性质:

  • 一条直线只有一个无穷远点;所有平行线有公共的无穷远点P
  • 任何两条不平行的直线有不同的无穷远点(否则会造成有两个交点)(P1,P2,P3..... Pn)
  • 射影平面上所有无穷远点(P1,P2,P3....Pn)构成连接成一条无穷远直线

那么,根据以上三个性质,我们定义了射影平面:所有的欧式平常直线和那一条无穷远直线构成了射影平面。也可以说,所有平常点与所有无穷远点构成了射影平面,还或者可以说,所有的射影直线构成了射影平面,本质都是一样的

哈哈,小学大家都知道,点构成线,线构成了面,面构成体,是吧。

因为在欧式平面上的平行线都有相同的无穷远点,不同的线又有不同的无穷远点,无穷远点又构成一条无穷远直线,所以射影平面上的任何一点都可以用三维的齐次坐标(X,Y,Z)表示,其中X,Y,Z不能全为0。

其中

  • 当 Z 不为 0,则该点表示欧氏平面上的点(X/Z,Y/Z)。 (欧式平面的平常点,构成了所有欧式直线)
  • 当 Z 为0, 则该点表示无穷远点 (无穷个无穷远点构成了无穷远直线)

这样我们就可以根据以上的坐标表示,把欧式平面上的点投影到射影平面中去。

例如,把一个欧式平面上的点(x,y),令 x = X/Z, y = Y/Z, Z != 0, 则投影到射影平面上的点就是(X,Y,Z)。 用具体数字代换进去就是,点(1,2)投影到射影平面的点就是(Z,2Z,Z),所以,诸如(1,2,1),(2,4,2)等这样形式的坐标表示是点,在射影平面内都算同一个点。

这时候关键的人物上场了,就是我们期待已久的椭圆曲线,它的定义就是:

  • 在射影平面上满足威尔斯特拉斯椭圆方程(Weierstrass)所有点的集合

该方程的表示是(注意,是三元方程):

该方程可以归整到欧式二维平面的方程表示形式:

其中a,b为常数系数,以下是a,b不同的取值,表现的椭圆曲线的欧式二维图像:

所以可以看到,椭圆曲线并不是椭圆形,这个是有历史原因的,因为椭圆曲线方程与求椭圆弧长的椭圆积分的反函数很像。

到这里大部分网络上会直接把上面的方程写出来,没有讲述过多的来源和发展历史。所以就以为椭圆曲线是一个二维的东西,实际上不是。因为投影的关系,我们可以降维到用二维的图像来研究它。

还有一个要注意的一点就是,椭圆曲线是非奇异的,也就是是光滑的,也就是各个分量的偏导数不能全为0。而且欧式二维平面的椭圆曲线因为有y^2项,所以一定关于x轴对称的,这个性质之后会用得到。

椭圆曲线上的运算

之前我们看到,椭圆曲线那么奇怪,歪瓜咧枣的线条是怎样跟密码学里关联上了?曲线上的这些点有什么联系呢? 依靠点与点之间的心灵感应吗? 哈哈,这里开个玩笑。

回顾下之前我们学的关于群的部分,我们要定义个点与点之间的的二元加法运算,这个运算在曲线上的点的集合上构成一个阿贝尔群。所以就涉及到怎样设计这个二元加法运算满足阿贝尔群公理就是个问题,跟初等代数的加法不一样哈。

那么设两个在椭圆曲线上的点P,点Q,那么P + Q就是连接PQ两点作直线,相交于椭圆曲线于一个点R’,那么过交点R‘作与x轴的垂线并延长再次与椭圆曲线相交于一个点R,那么P + Q = R。也就是点R’与点R关于曲线的x轴对称。

以下就是椭圆曲线上P + Q = R的加法运算的图像(网络上很多,随便找的):

根据上图,你可以在草稿纸上画,第一眼就可以看出来满足交换律, P + Q = Q + P = R,

然后你在设一个点K,然后再画直线,你会发现(P + Q) + K = P + ( Q + K)也满足结合律。

按照上面的定义,前提假设是P != Q,没法解释 P = Q 也就是P,Q两点重合的情况下,无法过两点作一条直线,这时候的加法运算是怎么定义的呢?其实只要过两点的重合点画出该点在椭圆曲线上的切线就可以了,然后规则与上一个图例是一样的:

上图就定义了P + P = 2P = R,这样的运算称为椭圆曲线上的二倍运算。

此外,如果我们将某一点A关于x轴对称位置的点定义为-A,这样的运算称为椭圆曲线上的正负取反运算,如下图:

上图就定义了类似我们印象中的负数,好了,如果我们将A和-A相加会怎样呢? 上图也说明了A+(-A)的情况,就是过这两点作一条直线,这条直线是与y轴平行的,唉?不对啊,这条直线与椭圆曲线没有第三个交点了啊? 其实不是的,因为椭圆曲线实际上并不是欧式二维平面的东西,它来自于非欧几何中的黎曼几何(黎曼球面),二维平面也展示不出来交点,我们就认为过点A和-A的这条直线与椭圆曲线相交在无穷远点,这个无穷远点我们一般记作O。所以就定义了 A + (-A) = O 。

好了,至此为止,我们对椭圆曲线上的点(包括无穷远点)的加法运算进行了定义,细心的人会发现,这个在椭圆曲线的点集和无穷远点点集上定义的加法运算满足了之前阿贝尔群(交换群)的公理, 无穷远点O就是单位元,任意一点A的逆元就是-A,并满足交换律,结合律。完美啊,perfect。

曲线上的运算规则已经清晰,当给定椭圆曲线方程及点的坐标时,我们就可以用坐标进行加,减,倍乘运算。比如,给定曲线上的一个点G(基点,base point),我们可以求2G,3G。其中3G = G + 2G,以此类推。也就是说,当给定G点时,已知标量k,求点kG(G的k倍)的问题并不困难。但反过来,已知点kG求标量k则非常困难。这就是椭圆曲线密码学中所利用的“椭圆曲线上的离散对数问题”,英文简称也就是所谓的ECDLP。

通过以上解释,已知G点的情况下,其实标量k也可以看作是私钥,kG看作公钥。从私钥计算公钥很简单,但是根据公钥推私钥,几乎不可能。这就是非对称的密钥对。

椭圆曲线上的离散对数问题

ECC利用了ECDLP的复杂度,RSA利用了质因数分解的复杂度,其本质都是利用了计算困难的问题,这深入的话题,研究计算理论,计算复杂度的学者会更有了解。

利用有限域这个工具对椭圆曲线进行离散化

椭圆曲线要形成光滑的曲线,其点的坐标(x,y)都必须是实数,即实数域R上的椭圆曲线。实数域R的椭圆曲线不合适用在密码学当中,计算机擅长于处理有限,并且离散的整数数据,所以要用在有限域Fp上的椭圆曲线(p为素数)。所以点坐标(x,y)这些都在有限域下了。更直观一点的话,就是在有限域限定的整数下来进行点的坐标运算,并且将结果除以p求余数。

例如: y^2 = x^3 + x + 1 (mod 23) 就是位于有限域F(23)上。也就是说对于在有限域F(23)下的任意一点(x,y),对于方程左侧的y^2的结果mod 23 与右侧的x^3 + x + 1的结果mod 23的结果相等。

上面的例子给定的素数p是23,这个素数很小,构成的椭圆曲线在有限域上的离散点的数量太少,所以在这个有限域上的ECDLP不难解,但是当素数p非常大的时候,要解这个问题就非常困难了,通常工业级的椭圆曲线的密码算法库给定的素数p都非常大。

以NIST推荐的一种椭圆曲线Curve P-521为例,其质数p就是下面这个长达157位(十进制位)的数。

>>> 2 ** 521 - 1
6864797660130609714981900799081393217269435300143305409394463459185543183397656052122559640661454554977296311391480858037121987999716643812574028291115057151

当然,为了提高计算效率,NIST推荐p的值使用梅森素数(或伪梅森素数),例如,在椭圆曲线P-256中,p = 2^156 - 2^224 + 2^192 + 2^96 - 1。

梅森素数目前仅发现51个。梅森数是指2^p - 1这类形式的数,其中p是素数,如果2^p - 1刚好也是素数,那么2^p - 1这个数就是梅森素数。梅森素数很少,梅森数越大,梅森素数就越难出现。

到现在,终于知道为什么人类一直在致力于发现新的素数了吧,其实素数是很有价值的,对科学。当然,也不尽然,因为我看了相关专业的人士评价,其实素数的发现,也仅仅集中在密码学领域有用途,其他方面素数价值并不是很大,人类致力于探寻也只是好奇驱使而已。有兴趣的可以看一看这个世界性的分布式网格计算项目GIMPS,该项目就是利用世界上计算机的闲置资源来计算并发现更大的素数。

有限域下的椭圆曲线

对于曲线y^2 = x^3 + x + 1在有限域F(23)下的椭圆曲线上的点(x,y)的坐标分布与顺序都是杂乱无章的。如下图:

设给定点P(3,10)关于P点的标量乘k(1-27)都是毫无规律的。

对于给定点P,那么P点的逆元-P(3,13),刚好27P的坐标就是(3,13),那么28P = 27P + P = -P + P = O (无穷远点)。如果满足这样的关系就称为P(3,10)的阶就是28。

这样就给出了一个关于在有限域椭圆曲线上的点阶的定义:

  • 如果椭圆曲线上一点P,存在最小的正整数n使得数乘nP = O ,则将n称为P的阶。
  • 若n不存在,则P是无限阶的

这个阶的作用在生成私钥的时候会用到,设曲线上的点G(base point,也称生成元),生成私钥k,那么公钥就是kG,其中k < n,且nG = O,也就是说,私钥k要小于G点的阶数n。

细心的人又会发现了,这个以生成元G点开始,假设G点的阶为n,那么由G生成的有限循环子群为:{O, G, 2G, 3G,..., (n-1)G}。

还不明白的话,又以上图举例:

  • 生成元G(3,10),生成元的逆元-G(3,13), 27G(3,13)
  • G = G
  • 2G = 2G

往下类推

  • 26G = 26G
  • 27G = -G
  • 28G = 27G + G = O
  • 29G = 28G + G = 0 + G = G
  • 30G = O + 2G = 2G

你会发现,无论是多少倍的G,都会被循环分解限定在其G点生成的循环子群内。

椭圆曲线点的坐标运算

设点P(x1,y1),点Q(x2,y2)的坐标,点R(x3,y3),那么计算R点的代换公式分以下几种情况:

  • 若P为无穷点(单位元),即P = O,此时R = P + Q = O + Q = Q;若Q为无穷点,即Q = O,此时R = P + Q = P + O = P;若P和Q都为无穷点,即 P= Q = O,则 R= P + Q = O + O = O。

  • 若 x1 = x2而 y1 = -y2,此时称Q点为P点的逆元,记为P = -Q = -P,且R = P + Q = O。(也就是P,Q两点关于x轴对称)

  • 若x1 = x2且y1 = y2,即P = Q,此时R = P + Q = 2P = 2Q,其中

上图的lambda是值是椭圆方程等式两边同时求导,这样就是当前切点的斜率

  • 除上述特殊情况之外的一般情况,即P != ±Q时,R = P + Q,其中

上图的lambda就是过两点求直线斜率的公式了。

以上公式给出来了,代码应该会写了吧,这里就不写了,因为椭圆曲线的坐标加法运算要在有限域Fp下进行,所以上面的公式后面都得mod p。

比如:

lambda = ((3*x1^2 + a) / 2*y1) mod p

x3 = (lambda^2 - 2*x1) mod p

y3 = (lambda*(x1 - x3) - y1) mod p

如果你细心观察上面的运算公式,你会发现lambda的值可能在计算机下会有错误,这种错误就是除数和被除数之前可能并不是一个整除关系的时候,得到的可能是0,或者小数,而有限域下,是不可能有小数存在的,这样计算会有错误。

比如 a = 3, b = 4, p = 7, lambda = (3 / 4) mod 7的计算结果就会有问题,3/4并不是一个整除关系,如果这样计算,计算机最后计算的lambda结果就是0。所以需要把除发转化到乘法逆元,(3 / 4) mod 7 = (3 * 4的逆元) mod 7。

在mod运算下,计算乘法逆元最朴素的方案是用扩展欧几里德算法去求。但是求逆运算性能贼慢,优化方法有很多, 主要思路都是尽量避免求逆,一个是降低总计算步骤中的求逆次数,一个是转换椭圆曲线的坐标系直接避免求逆。这里深入的暂时不介绍,网上论文很多,比如: 《GF(3^n)下的椭圆曲线快速算法研究》,这篇论文是北航的,主要是降低求逆次数,对于kG点,其中标量k需要满足3^n的形式,并且对于椭圆方程y^2 = x^3 + a*x + b中的a,b系数也限定在有限域F(3^n)的形式。

就拿普通的点加运算来说,注意看P != Q的点加公式,lambda计算上是1次逆运算,1次乘法运算,x3的计算上又是1次平方运算,y3的计算上是1次乘法运算,当然,公式中的加减法的运算开销可以忽略不计,关键就是以上的三种运算,逆运算,平方运算,乘法运算开销最大,其中1次逆运算更是相当于1次乘法运算开销的10倍。

下面来举个例子就知道了,为了方面直观,直接a,b满足整除关系就好了:

(4 / 2) mod 7 = 2

上式可以转换为 (4 * inverse(2,7)) mod 7 = (4 * 4) mod 7 = 16 mod 7 = 2

其中inverse(2,7)是求2在模7运算下的乘法逆元,我用python计算的,结果为4。可以看出,转化为乘法逆元,其结果是一样的,最终都是等于2。

至此为止,不考虑运算性能的话,只要求逆搞定了,那么椭圆曲线上的所有运算都可以根据公式搞定了。P + Q, kP = P + (k - 1)P, 2P = P + P,3P = 2P + P。反正点的标量乘法,最终都可以退化成点加法。

这里注意哈,这里的乘法逆元跟曲线上的点的加法逆元不一样!

比如设素数域F(23)的椭圆曲线上的点P(3,10),那么点P的逆元-P就是(3, -10 mod 23),-P的坐标就是(3, 13)。但是10在模23下的乘法逆元inverse(10,23)的值是7。这是完全不同的两个运算,别搞错了。

椭圆曲线的Diffie-Hellman密钥交换(ECDH)

我们上一小节知道了利用椭圆曲线求密钥对的方式,那么接下来看看更具体的应用。比如利用椭圆曲线进行DH交换。这个流程大致与朴素的DH交换的流程相同。

  • 朴素的DH交换利用的是以p为模,已知G和G^x mod p 求x的复杂度(有限域上的离散对数问题)。

相对地,椭圆曲线DH交换利用的是:

  • 在椭圆曲线上,已知G和xG 求x的复杂度(椭圆曲线的离散对数问题)

以密码学流行的Alice和Bob对话的例子,假设,Alice和Bob需要共享一个对称密码的密钥,然而,双方之间的通讯线路已经被窃听者Eve窃听,这时候就可以利用ECDH来进行安全的密钥交换。

ECDH的流程大致是以下顺序:

  • Alice向Bob发送点G(基点),这时候点G被Eve知道也没关系
  • Alice生成随机数a,这个数是自己生成的留着自己用。所以不会发送出去告诉Bob,也不可能会被Eve知道。(由椭圆曲线的知识点得知,其实随机数a是Alice的私钥。)
  • Bob生成随机数b,这个数是自己生成的留着自己用。所以不会发送出去告诉Alice,也不可能会被Eve知道。(b是Bob的私钥)
  • Alice向Bob发送点aG,点aG被Eve知道也没关系,因为aG是Alice的公钥,公钥本来就可以公布。
  • Bob向Alice发送点bG,点bG被Eve知道也没关系,因为bG是Bob的公钥。
  • Alice收到Bob的公钥点bG的时候,再次对点bG进行椭圆曲线上的a倍的标量乘法得到点abG。
  • Bob收到Alice的公钥点aG的时候,再次对点aG进行椭圆曲线上的b倍的标量乘法得到点baG。
  • 因为点abG等于点baG,所以它们就是Alice和Bob的共享密钥

简单来说,就是ECDH就是利用双方共同的G点,各自生成自己的密钥对,然后进行公钥交换,然后收到对方的公钥再次利用自己的私钥做椭圆曲线上的标量乘法。所得到的点是同一个点。这个结果点就是共享密钥。

Eve窃听到的数据只有G,aG,bG,所以无法求出a,b,自然也无法求abG这个共享密钥。

但是严格意义上来说,椭圆曲线上的的DLP只能证明已知G,aG,bG难以求a,b。但不能证明已知G,aG,bG难以求abG。所以后者是有另外的论文证明的,反而不是椭圆曲线的DLP证明的。

所以,ECDH是安全的,别担心。

在工业实践中,往往Alice Bob的每次通信都需要更换随机数a,b,这样即便在某个时间点通信的机密被破解,由于每次通信使用的共享密钥不同,也无需担心通信内容被破解。这样的特性称为前向安全性(Forward Secrecy,FS)或完全前向安全性(Perfect Forward Secrecy,PFS)。比如,SSL/TLS使用椭圆曲线密码时,选择ECDHE_ECDSA和ECDHE_RSA密钥交换算法都可以获得这特性。

如果细心的人可以发现,ECDH无法防止中间人攻击,比如Eve不仅仅是窃听通信,而是冒充代替Bob。冒充问题其实可以用之后的数字签名解决。

椭圆曲线ElGamal密码

好了,由上一节Alice Bob都生成了共享密钥abG,接下来就要用abG这个密钥加密通信的message了。

假设,Alice要向Bob发送一条Message,Alice可以将自己要发送的Message用椭圆曲线上的一个点M表示(实际上Message是编码成该点的x坐标,这样点M的y坐标也知道了)。

  • Alice对消息M计算点M + abG。此点M + abG就是密文。
  • Alice将密文M + abG发送给Bob。Eve当然不知道密钥abG,所以不可能破解密文。
  • Bob收到密文M + abG后,当然可以解密出消息M。就是M + abG - abG = M

椭圆曲线的数字签名算法(ECDSA)

大家都知道利用非对称密钥对可以进行数字签名,所以是可以利用椭圆曲线密码进行数字签名的。这样的签名方案是ECDSA。

假设,Alice要对消息m加上数字签名,而Bob需要验证该签名。(注,以下写着计算这个关键词的其实都是以mod p的运算,毕竟是有限域下的椭圆曲线)

  1. 生成数字签名
  • Alice生成自己的随机数r和基点G,求出点rG = (x,y)。
  • Alice根据随机数r,消息m的散列值h(h = hash(m)),私钥a计算s = (h + ax)/ r 。
  • 最后,Alice将消息m,点rG = (x,y)和s发送给Bob,其中点rG和s就是数字签名。一个二元组。
  1. 验证数字签名
  • Bob收到消息m,点rG = (x,y)和s
  • Bob根据消息m求出散列值h(h = hash(m))
  • Bob根据上述信息,用Alice的公钥(aG)进行以下计算:

(h / s)G + (x / s)(aG) => ((h + ax) / s)G => 代换s: (r(h + ax) / (h + ax))G => rG

由上述的推导过程可发现,如果计算的结果与rG一致,那么验签成功,反之失败。

这个过程中,攻击者Eve不知打Alice的私钥a,所以无法计算出合法的s,即便对于同一条消息m,只要改变随机数r,所得到的数字签名也会改变。

总结

本文只给出了ECC的线性知识的思路,包括从哪里到哪里,但是没有涉及算法优化等细节。有机会我再研究研究,并再写个文章分享出来吧。:)

windows用户态程序的dump文件生成


title: windows用户态程序的dump文件生成
date: 2018-06-03 12:58:23
tags:
- 操作系统
- Win32

熟悉Linux的开发人员都知道,在Linux下开发程序,如果程序崩溃了,可以通过配置Core Dump,来让程序崩溃的瞬间产生一个Dump文件,然后通过dump文件来调试程序为什么崩溃。但是windows下就比较麻烦。

windows下配置用户态程序的Dump非常麻烦

总结后第一种的方式是用一个微软出品的官方工具,是一个UserDump.exe的命令行工具。

用法是: 下载下来,安装,解压到特定目录,里面就可以看到userdump.exe这个命令行工具了,把这个命令行工具随着公司的产品软件打包,在产品软件中的main函数中编写如下代码:

LONG __stdcall
MyUnhandledExceptionFilter(
    EXCEPTION_POINTERS *ExceptionInfo
)
{
    // Invoke userdump here.
    // The implementation is similar to MyFilterFunction,
    // above.

    (void)ExceptionInfo;

    QString dumperExe = QString("%1/%2/%3").arg(qApp->applicationDirPath()).arg("dumper").arg("userdump.exe");

    QString cmdLine = QString("%1 -k -w %2").arg(dumperExe).arg("TRMSMonitor.exe"); // product.exe

    QProcess* startDump = new QProcess;
    startDump->start(cmdLine);
    startDump->waitForFinished(-1);

    return EXCEPTION_EXECUTE_HANDLER;
}

int main(int argc, char* argv[])
{
    QApplication a(argc, argv);

    SetUnhandledExceptionFilter(MyUnhandledExceptionFilter);

    //主程序单例模式
    RunGuard guard("TRMS");

    if (!guard.tryToRun()) {
        exit(-1);
    }

    // any code here

    return a.exec();
}

以上代码的意思是,在程序崩溃的瞬间,未处理异常的时刻,调用userdump命令行来生成程序自身的Dump文件。

方法二,修改系统注册表的方式来让系统自动产生Dump,最推荐这样的方式,这种方式最准确。用法戳这里

如果WinDbg下载了符号表,那么直接可以把产生crash的源码崩溃地点显示出来:

Microsoft (R) Windows Debugger Version 6.12.0002.633 X86 Copyright (c) Microsoft Corporation. All rights reserved. Loading Dump File [C:\crashdumps\mid.exe.2324.dmp] User Mini Dump File with Full Memory: Only application data is available WARNING: Minidump contains unknown stream type 0x15 WARNING: Minidump contains unknown stream type 0x16 Symbol search path is: srv*c:\symbols*http://msdl.microsoft.com/download/symbols Executable search path is: Windows 7 Version 16299 MP (4 procs) Free x64 Product: WinNt, suite: SingleUserTS Machine Name: Debug session time: Fri May 4 10:57:03.000 2018 (UTC + 8:00)
System Uptime: 0 days 23:20:34.747
Process Uptime: 0 days 0:00:03.000
........................................
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(914.35c4): Access violation - code c0000005 (first/second chance not available)
ntdll!ZwWaitForMultipleObjects+0x14:
00007ffe`5c5b0e14 c3 ret
0:000> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
 
Failed calling InternetOpenUrl, GLE=12029
 
FAULTING_IP: 
mid!CJASE2000App::InitInstance+3c [d:\furen-work\furen-projects\tuxedo-projects\zjj\mid\jase2000.cpp @ 223]
00007ff6`ced7aa9c 45892424 mov dword ptr [r12],r12d
 
EXCEPTION_RECORD: ffffffffffffffff -- (.exr 0xffffffffffffffff)
ExceptionAddress: 00007ff6ced7aa9c (mid!CJASE2000App::InitInstance+0x000000000000003c)
 ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
 Parameter[0]: 0000000000000001
 Parameter[1]: 0000000000000000
Attempt to write to address 0000000000000000
 
DEFAULT_BUCKET_ID: NULL_POINTER_WRITE
 
PROCESS_NAME: mid.exe
 
ERROR_CODE: (NTSTATUS) 0xc0000005 - 0x%p
 
EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - 0x%p
 
EXCEPTION_PARAMETER1: 0000000000000001
 
EXCEPTION_PARAMETER2: 0000000000000000
 
WRITE_ADDRESS: 0000000000000000
FOLLOWUP_IP: 
mid!CJASE2000App::InitInstance+3c [d:\furen-work\furen-projects\tuxedo-projects\zjj\mid\jase2000.cpp @ 223]
00007ff6`ced7aa9c 45892424 mov dword ptr [r12],r12d
 
MOD_LIST: <ANALYSIS/>  
NTGLOBALFLAG: 0
 
APPLICATION_VERIFIER_FLAGS: 0
 
FAULTING_THREAD: 00000000000035c4
 
PRIMARY_PROBLEM_CLASS: NULL_POINTER_WRITE
 
BUGCHECK_STR: APPLICATION_FAULT_NULL_POINTER_WRITE

LAST_CONTROL_TRANSFER: from 000000005e44ccae to 00007ff6ced7aa9c
 
STACK_TEXT: 
00000000`00cff3a0 00000000`5e44ccae : 00000000`00000001 00000000`00000001 00007ff6`ced30000 00000000`00000000 : mid!CJASE2000App::InitInstance+0x3c [d:\furen-work\furen-projects\tuxedo-projects\zjj\mid\jase2000.cpp @ 223]
00000000`00cff710 00007ff6`ceda05e3 : 00000000`02eb4383 00000000`00000000 00000000`00000000 00000000`00000000 : mfc100!AfxWinMain+0x76
00000000`00cff750 00007ffe`5a8d1fe4 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : mid!__tmainCRTStartup+0x15f [f:\dd\vctools\crt_bld\self_64_amd64\crt\src\crtexe.c @ 547] 00000000`00cff800 00007ffe`5c57f061 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0x14
00000000`00cff830 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
 
 
FAULTING_SOURCE_CODE: 
 219: // registerExceptionHandler(); 
220: 
 221: int *pSS = NULL;
 222: > 223: *pSS = 0;
 224: 
 225: m_hMutex = CreateMutex( 
226: NULL, // no security descriptor
 227: FALSE, // mutex not owned
 228: "KD20_Transaction"); // object name
 
SYMBOL_STACK_INDEX: 0
 
SYMBOL_NAME: mid!CJASE2000App::InitInstance+3c
 
FOLLOWUP_NAME: MachineOwner
 MODULE_NAME: mid
 
IMAGE_NAME: mid.exe
 
DEBUG_FLR_IMAGE_TIMESTAMP: 5aebcbd4
 
STACK_COMMAND: dt ntdll!LdrpLastDllInitializer BaseDllName ; dt ntdll!LdrpFailureData ; ~0s; .ecxr ; kb
 
FAILURE_BUCKET_ID: NULL_POINTER_WRITE_c0000005_mid.exe!CJASE2000App::InitInstance
 
BUCKET_ID: X64_APPLICATION_FAULT_NULL_POINTER_WRITE_mid!CJASE2000App::InitInstance+3c
 
WATSON_STAGEONE_URL: http://watson.microsoft.com/StageOne/mid_exe/2_0_0_0/5aebcbd4/mid_exe/2_0_0_0/5aebcbd4/c0000005/0004aa9c.htm?Retriage=1
 
Followup: MachineOwner

EOF

逻辑判断


title: 逻辑判断
date: 2014-06-27 11:34:36
tags:
- 逻辑

本人非原始作者,原作者是一个网名为mingda1986的清华物理系毕业生,现在在MIT读PhD,想必已经PhD毕业了

在对一件事下定论时,我们离不开推理论证,这需要真实客观的信息,合理的逻辑推理,有时外加人性的考量。然而,目前的教育方式以及社会环境,在这方面比较欠缺,甚至有意误导,常让人获得错误的结论。信息了解不全面,影响判断;因果推理不正确,对无关事物强加因果,或者只是敷衍地给出站不住脚的理由;再或者把谎话重复一千遍,听多了就像是真理。不知不觉的,就在方方面面的损害了人的判断力。

本文旨在提供一点线索,减少垃圾信息的侵害,用理性和良知武装自己的大脑。本文仅能管中窥豹,望读者举一反三。

常见破坏逻辑的方式如上所提,即信息不客观,逻辑不合理,或者不考虑人性。

一、不客观的信息: 对于无法辩驳的事实,选择性隐瞒
一个更好的做法是,你有权利了解发生过的事实,并且可以看到不同的评价,然后自己去做判断。

二、错误推理: 强行绑定事物,使之发生因果关系

这种破坏逻辑的方式比第一种更加隐蔽;因为第一种方式里,只要知道足够客观的事实,偏颇的观点自然攻破。然而,有很多事物之间存在关联,但并非因果关系。因此,稍有疏忽,就会相信了这些本来没有因果关系。如果教育过程中,强行让本来不属于直接推出关系的两件事, 对大脑的逻辑能力非常有害。

因果关系是说,假设A是前因,B是后果,那么它们之间至少有如下特征:

  • 它们协变。即A的变化引起B朝着某个方向的变化,具有相关性。
  • A发生早于B。即符合相对论,信号传递速度有限。
  • A与B不是伪关系。所谓伪关系,即A与B表观上有相关性,其实它们都由与其他因素左右才显得相关,而不是因果性。

前两者比较容易理解,常见的逻辑混乱出在第3点上,即把仅有相关性事物(如,时间/空间相关)说成是因果性。

三、不考量人性: 避重就轻,转移注意力

这一种损害逻辑的方式,比上一条更加隐蔽。在强加因果关系的过程中,你可能会对一些理由感到很不舒服,好像遇到了”混蛋逻辑“,或者强词夺理。即使因为种种原因(强硬的标准答案威慑),你没能战胜这个错误的因果关系,至少还能意识到它不太对劲。然而,避重就轻转移注意力,是真正隐蔽而有害的。它把你的精力,从对自己有益的关注,扯到没有意义的事情上,自己却浑然不觉,因为不知道到底什么是真正重要的事情。

我以为,一些事实内容,应当作为常识,让小学生去了解,而不是让他们欢迎领导,或者天天告诉他们要爱这个爱那个。这样建立起来的价值观,才能更好的在一些共同的假设面前做逻辑演绎和辩论,并且有着更人性的考量,而不是遇到不可逾越的交流障碍,驴唇不对马嘴。

附录 常见逻辑错误以及例子

  • 神逻辑: zx委员陈红天说:“各种手段都没把房地产压下去,说明没有泡沫。”

这真的属于神逻辑,因为常见的逻辑错误都没有这样明显的。如果发生这种情况,就是基本的假设前提都没有共同点,很难找到一个共同的起点进行沟通或者辩论。

  • 自相矛盾(Inconsistency):年仅20岁的国家滑翔伞队队员杨小强在完成一个高难度技术动作时出现失误,结果从百米高空坠落,当120赶到事发地时,杨小强已停止呼。一位国家体育总局工作人员向本报透露:“杨小强无国家队编制,应该不能按工伤处理”。

在这里,杨小强已经是国家队队员,但是却又说他“无国家队编制”,自相矛盾,让人称奇。实际上,在第六届世界滑翔伞定点锦标赛中,杨小强获得亚军,这是**选手在该项比赛中取得的最好成绩。杨小强在国家滑翔伞队队员选拔赛中获得第六名,由此入选国家队,所以是正式的国家队队员。什么叫没有编制?

  • 偷换概念(disguised displacement):卫生部医管司副司长孙阳做客人民网。孙阳说,“看病难、看病贵是世界性难题,即使是一些发达国家,这个问题也没有完全解决。”

这里用“完全解决”偷换“几乎无所作为”。完全解决医疗问题也许是难题,但是现在的状况相差甚远,比如根据世界卫生组织的排名,医疗公平性位居全世界倒数第四。

  • 混淆相关和起因(confusion of correlation and causation): 温州乐清有一个标语,说“上坟引发火灾,坟墓一律拆除”。

这就是本文提到的把相关性强制因果。况且,“上坟”和“火灾”的相关系数极低,只需禁止明火上坟,相关系数可以降为零。后面一句话,就开始拆除?!

  • 片面辩护(special pleading): 2012年,郑州拆除了全市的所有报刊亭,该市成了全国唯一没有报刊亭的省会级城市。拆除之后,郑州市相关部门解释,”原有报刊亭存在违章占道、阻碍交通、违规经营等问题,清理目的是:退店经营、还路于民。今后,市民可在超市、书店、加油站或门店购买报刊、杂志,报刊销售点比之前的报亭数量将 大幅增加。“

报刊亭只占用部分人行道,所以不会阻碍交通。报刊亭往往处在客流量很大的地方,给人买报纸带来方便。因为很多人坐公车可以看报,加油站、书店数量少得多。而且开车不能看报。所以是否能增加销售点数量存疑。即使增加销售点数量,客流量也未必会更大。这是片面辩护,只看到了好,没看到最大的弊端:给人们买报这个基本需要带来极大不便。

  • 稻草人谬误(straw man): 人民日报说,因为部分腐败现象就批评国家是极端主义。”因为有消极腐败现象,就把国家说得一无是处;因为有为富不仁,就对所有富人怨、恨、怒。“

所谓稻草人谬误,就是故意攻击一个不存在的情况。比对于为富不仁,人们恨的是为富不仁本身,不但”不仁“,而且更加邪恶,但不是很所有的富人。仇富仇的是不公平的获得钱财的方式,不是因为一个人有钱就要恨。所以这个情况虚设了一个靶子,然后对其进行攻击。

  • 小众统计(statistics of small numbers): 人大常委郑功成在接受采访时表示,大学生一毕业就想买房,心态不正常,美国也没有这么高的住房自有率。他反问称二三十岁的时候,住着国家提供的房子,有什么不好?

这里的问题就是小众统计。拿一小部分人的样本,来说明普遍的问题。在发达国家,大学毕业生工作5年即可自有房产。然而20-30岁的时候,住着国家提供的房子的人,是极少数的一部分小众,不具有代表性。

  • 推不出或不当结论(non sequitur): 在黑龙江亚布力论坛上,浙江副省长、温州市委书记陈德荣发表演讲称,自去年七八月开始的金融风波,不能把所有的账算到温州人头上。现在有一点妖魔化温州,对温州是不公平的。他还说,其中有一些企业家,把命都搭上去了,这种方式不可取,但这恰恰是企业家的精神。

假设跳楼属于某一种企业家精神,那么跳楼又不可取。所以企业家精神至少有一种不可取。所以企业家精神不值得赞扬。这里完全忽视人的生命,况且企业家精神到底是个什么精神?这是很不恰当的言语。从跳楼也根本推不出这个结论。

  • 转移话题(red herring): 针对近期媒体报道“多地小学生被要求加入红十字会并缴会费”的情况,**红十字会总会10月14日回应称,已向各有关红十字会调查情况,收取会费目的是增强会员意识,所缴会费将全部用于为会员提供服务以及开展与红十字会宗旨相一致的各项活动。

入会收取会费,也许无可厚非,但是这里的关键不在于收费的目的,也不在于它到底要用这笔钱做多少公益事业,而在于入会的过程是否是强制还是完全自愿。如果背离了自愿入会,而是半强制的集体暴力,统一入会,那就违背了它的初衷,和它到底要拿钱干多少好事无关。这种回应就属于转移话题。

这一个逻辑错误在官方回应的时候非常常见,常常答非所问,值得注意。

  • 含糊不清(Ambiguity) :吉林大学白求恩第一医院(以下简称吉大一院)在网站上公布新建干部病房大楼图片,被网友冠以“八星级”豪华干部病房头衔,引发热议。一些网民担心这会挤占公共医疗资源。吉大一院回应称,干部病房只占一小部分,且造价并非所谓天价。

”占一小部分“和”并非天价“都是很模糊的词汇。到底一小部分是多少?非天价又是多少钱?当然,关键在于”这些病房的豪华“这个陈述是否属实。所以不回应豪华反而王顾左右而言他,也是一种转移话题。

推广思考---**政治课的洗脑

其目的根本就不是让你相信马克思主义那一套,当权者也心知肚明,平民骗得了第一代但是第二代就骗不了,他们也不指望有多少人这些光喊口号的东西(相反,如果**人真的傻到这样,国家早就没救了),特别在信息化时代,这不可能做到。

他们的真正目的是,通过这些不符合逻辑的教育来消灭学生独立并且理性思考的能力。逻辑和理性其实和数学物理等科目一样,也是需要教育(或者训练)的(例如西方古典教育有专门对逻辑、辩论等进行教育),如果缺少训练,那么日后就难以形成该习惯。当然了,缺乏独立思考能力的人是**者希望的结果,而且这样就足够了。

所以**学生以为看破了洗脑教育这一套,却没想到有更大的一盘棋,例如有些人为了反对而反对,结果又走向另一个极端。例如看一下网上的各种势力,除了被教科书洗脑的五毛党和毛左外,还有不是被教科书洗脑,但却一样不理性的兔粉、皇汉、黄皮纳粹、还有各种无脑黑。这就是缺少逻辑教育的体现。

用马尔科夫链来做自动补全


title: 用马尔科夫链来做自动补全
date: 2019-03-10 15:26:00
tags:
- 马尔科夫链
- 自然语言处理

前言

马尔科夫链是个什么呢?如果你去网上搜索, 噼里啪啦一堆公式,可能你会看的不耐烦了。当然,你可能见到有些人总结了一句经典的话,比如:

  • 如果将下一个状态的依赖条件,简化成:仅取决于当前状态,和之前其他状态无关。那么,这个随机过程就是马尔可夫链。

  • 今天的事情只取决于昨天,而明天的事情只取决于今天,与历史毫无关联

当然,你可以在知乎这里看到更多的解释。

这样的性质就给我们带来了预测下一个状态带来了可能,比如天气预报,智能联想提示等等。

今天我们就用python来用马尔科夫链来做点有实际意义的事情,用更直观的角度理解它。

正文是我用python脚本来分析了一本英文书,把这本书的英文句子作为一个训练集,用马尔科夫链来做处理产生短文,短文句子有一定“联想”,这样也可以算作是一个自动补全的程序了。比如,一些聊天工具和输入法可能就有这样的功能,基于你以前历史的语句输入来判断你当前输入后面可能会输入的单词或语句作提示或者补全。

网络社区也有一些爱好者用马尔科夫链来生成鸡汤文, 都算是一定性质的科普文章。

正文

首先,需要随便下载一个英文小说或者书籍来作为训练集合,英文当然越正宗越地道越好,这样的数据质量比较高,之后生成的段落语句结果也更好些。这里我下载了哈利波特1-7的英文txt。哈哈,英文原版我只读了第一部魔法石,惭愧啊。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# python 3! due to random.choices()

import operator, random
from collections import defaultdict



def remove_empty_words(l):
    return list(filter(lambda a: a != '', l))

def update_occ(d, seq, w):
    if seq not in d:
        d[seq]=defaultdict(int)

    d[seq][w]=d[seq][w]+1

# https://docs.python.org/3/library/random.html#random.choice
def gen_random_from_table(t):
    return random.choices(list(t.keys()), weights=list(t.values()))[0]

def process(): 
    with open ("harry-potter.txt", "r") as myfile:
        data=myfile.read()

    sentences=data.lower().replace('\r',' ').replace('\n',' ').replace('?','.').replace('!','.').replace('“','.').replace('”','.').replace("\"",".").replace('‘',' ').replace('-',' ').replace('’',' ').replace('\'',' ').split(".")

    # key=list of words, as a string, delimited by space
    #     (I would use list of strings here as key, but list in not hashable)
    # val=dict, k: next word; v: occurrences
    first={}
    second={}

    for s in sentences:
        words=s.replace(',',' ').split(" ")
        words=remove_empty_words(words)
        if len(words)==0:
            continue
        for i in range(len(words)):
            if i>=1:
                update_occ(first, words[i-1], words[i])
            if i>=2:
                update_occ(second, words[i-2]+" "+words[i-1], words[i])

    text=["magic", "is"]

    text_len=len(text)

    for i in range(200):

        last_idx=text_len-1

        tmp=text[last_idx-1]+" "+text[last_idx]
        if tmp in second:
            new_word=gen_random_from_table(second[tmp])
        else:
            # fall-back to 1st order
            tmp2=text[last_idx]
            if tmp2 not in first:
                # dead-end
                break
            new_word=gen_random_from_table(first[tmp2])

        text.append(new_word)
        text_len=text_len+1

    print (" ".join(text))

# main function
process()

运行以上代码,就可以生成一个150来个单词的"联想"短文了:

magic is not having practised vanishing spells horribly difficult at first but they two thousand racing broom tampering and torture; 
an interview said harry leaping to their bulky backpacks the nine of the cottages were fewer here and make it too far; 
it rolled over toward him holding a wand to shoulder wands raised james glancing over his head pound so he went as red as ron who clambered to
 his determination to shut his eyes wanting to know anything new right before you go upstairs with ron eating their way around the walls and 
 all their shock and exhaustion that the phoenix but after a few horrible seconds he did to open the lestranges vault only once a month 
 later harry kept his eyes was twitching slightly hair whipped back off the 
 pitch but all three of my followers would give better cover they extinguished their fire then he sat down when something of a voice 
 and called out of pure venom at harry his temper getting the exit and when sounds of someone stumbling ftom a room like a venomous bubble compressing his lungs driving all other concerns fled 
 harry s head with a large fir tree blocking the top

当然这段短文有点狗屁不通,甚至有点搞笑,我们现在先不管。

我们先来看看代码中的first和second变量,它们是一个字典。

first变量表示第一个单词后出现的单词的概率统计,这是一阶马尔科夫链生成的数据结果。

我们通过python把first的结果排序打印出来好了:

print ("first table:")
for k in first:
    print (k)
    # https://stackoverflow.com/questions/613183/how-do-i-sort-a-dictionary-by-value
    s=sorted(first[k].items(), key=operator.itemgetter(1), reverse=True)
    print (s[:20])
    print ("")
harry
[('s', 1344), ('and', 776), ('had', 657), ('was', 475), ('could', 400), ('potter', 385), ('said', 358), ('saw', 271), ('looked', 241), ('felt', 238), ('ron', 233), ('who', 201), ('thought', 172), ('asked', 163), ('as', 160), ('knew', 156), ('did', 125), ('i', 115), ('to', 108), ('in', 106)]

potter
[('and', 63), ('s', 59), ('said', 28), ('is', 26), ('you', 23), ('has', 21), ('i', 18), ('the', 17), ('was', 13), ('he', 13), ('sir', 13), ('will', 10), ('had', 10), ('she', 10), ('in', 9), ('must', 9), ('that', 9), ('stinks', 7), ('not', 6), ('come', 5)]

and
[('the', 1225), ('hermione', 954), ('he', 907), ('harry', 662), ('a', 605), ('then', 554), ('ron', 550), ('i', 541), ('his', 363), ('george', 341), ('it', 298), ('they', 289), ('you', 253), ('she', 243), ('was', 232), ('that', 211), ('there', 190), ('saw', 187), ('said', 171), ('looked', 161)]

the
[('door', 770), ('dark', 637), ('room', 629), ('other', 548), ('ministry', 477), ('first', 455), ('floor', 408), ('same', 386), ('way', 362), ('end', 362), ('castle', 358), ('air', 334), ('ground', 326), ('only', 326), ('back', 317), ('last', 296), ('table', 292), ('rest', 284), ('front', 263), ('death', 260)]

sorcerer
[('s', 24), ('in', 3), ('of', 1), ('spoke', 1)]

好了,我们现在再来看看second变量的结果,它表示两个单词之后出现的每个单词的概率统计,这是二阶马尔科夫链的结果,依次类推,还有3阶马尔科夫链,只不过代码里面没写。

print ("second table:")
    for k in second:
        print (k)
        # https://stackoverflow.com/questions/613183/how-do-i-sort-a-dictionary-by-value
        s=sorted(second[k].items(), key=operator.itemgetter(1), reverse=True)
        print (s[:20])
        print ("")
which made
[('the', 3), ('it', 2), ('harry', 2), ('drills', 1), ('professor', 1), ('ginny', 1), ('his', 1), ('ron', 1), ('him', 1), ('a', 1)]

he was
[('going', 96), ('not', 88), ('a', 66), ('still', 65), ('sure', 50), ('in', 47), ('doing', 43), ('the', 42), ('looking', 40), ('wearing', 38), ('so', 32), ('about', 29), ('trying', 29), ('on', 27), ('being', 25), ('very', 22), ('lying', 22), ('standing', 22), ('too', 19), ('supposed', 19)]

was a
[('very', 48), ('loud', 39), ('pause', 35), ('great', 35), ('long', 25), ('bit', 22), ('little', 20), ('good', 19), ('large', 16), ('moment', 16), ('flash', 15), ('few', 14), ('small', 14), ('tiny', 13), ('wizard', 12), ('bang', 12), ('nasty', 11), ('knock', 11), ('sudden', 11), ('lot', 10)]

a big
[('mistake', 3), ('deal', 3), ('plastic', 2), ('supporter', 2), ('party', 2), ('man', 2), ('beefy', 1), ('city', 1), ('argument', 1), ('old', 1), ('photograph', 1), ('green', 1), ('lumpy', 1), ('black', 1), ('red', 1), ('thing', 1), ('drop', 1), ('enough', 1), ('bet', 1), ('bearded', 1)]

big beefy
[('man', 1)]

好了,这里先暂时止步探索,你会发现二阶马尔科夫链的结果更加精确,生成的数据集也更加小。这不废话嘛,就像猜谜语一样,提示给的越精确越多,那么你猜中的概率越大,解的空间范围就缩小了,second表也就更小了。哈哈,开始有点意思了。

好的,我们现在在二阶马尔科夫链的基础上加入3阶马尔科夫链,按照2阶的代码修改就行了:

    first={}
    second={}
    third={}

    for s in sentences:
        words=s.replace(',',' ').split(" ")
        words=remove_empty_words(words)
        if len(words)==0:
            continue
        for i in range(len(words)):
            if i>=1:
                update_occ(first, words[i-1], words[i])
            if i>=2:
                update_occ(second, words[i-2]+" "+words[i-1], words[i])
            if i>=3:
                update_occ(third, words[i-3]+" "+words[i-2]+" "+words[i-1],words[i])
were proud to
[('say', 1)]

proud to say
[('that', 1)]

to say that
[('he', 3), ('she', 3), ('i', 2), ('they', 1), ('s', 1), ('you', 1), ('his', 1), ('from', 1), ('your', 1), ('at', 1), ('on', 1), ('age', 1), ('dumbledore', 1), ('it', 1)]

say that they
[('were', 1), ('know', 1)]

that they were
[('not', 9), ('all', 7), ('there', 3), ('going', 3), ('alone', 2), ('being', 2), ('indeed', 2), ('in', 2), ('a', 2), ('the', 2), ('still', 2), ('supposed', 2), ('no', 2), ('really', 2), ('perfectly', 1), ('related', 1), ('proud', 1), ('witches', 1), ('fifty', 1), ('already', 1)]

从以上可以看出,3阶显然更精确,并且third表也更小了。

以下为了让各位观众更清晰的看到3阶马尔科夫链的生成结果更准确,我用更加可读的输出打印出来:

    tests=["i can tell", "who did this", "she was a", "he was a", "i did not",
        "wanted to do", "you will find"]

    for test in tests:
        test_words=test.split(" ")

        test_len=len(test_words)
        last_idx=test_len-1

        if test_len>=3:
            tmp=test_words[last_idx-2]+" "+test_words[last_idx-1]+" "+test_words[last_idx]
            if tmp in third:
                print ("* third order. for sequence:",tmp)
                print_stat(third[tmp])

        if test_len>=2:
            tmp=test_words[last_idx-1]+" "+test_words[last_idx]
            if tmp in second:
                print ("* second order. for sequence:", tmp)
                print_stat(second[tmp])

        if test_len>=1:
            tmp=test_words[last_idx]
            if tmp in first:
                print ("* first order. for word:", tmp)
                print_stat(first[tmp])
        print ("")

输出:

* third order. for sequence: i can tell
you 42%
yeh 14%
who 7%
he 7%
mum 7%
* second order. for sequence: can tell
me 25%
you 20%
us 10%
yeh 5%
him 5%
* first order. for word: tell
you 17%
me 14%
him 10%
us 7%
them 5%

* second order. for sequence: did this
mean 27%
but 9%
year 9%
weird 9%
band 9%
* first order. for word: this
is 10%
time 6%
was 5%
year 2%
one 2%

* third order. for sequence: she was a
witch 12%
bit 12%
great 8%
very 8%
freak 4%
* second order. for sequence: was a
very 3%
loud 3%
pause 2%
great 2%
long 2%
* first order. for word: a
few 2%
little 2%
bit 1%
moment 1%
very 1%

* third order. for sequence: he was a
bit 7%
good 7%
wizard 6%
death 6%
big 4%
* second order. for sequence: was a
very 3%
loud 3%
pause 2%
great 2%
long 2%
* first order. for word: a
few 2%
little 2%
bit 1%
moment 1%
very 1%

* third order. for sequence: i did not
want 12%
have 8%
know 8%
ask 4%
fasten 4%
* second order. for sequence: did not
know 8%
want 7%
seem 6%
answer 4%
look 3%
* first order. for word: not
to 7%
be 3%
a 2%
have 2%
the 1%

* third order. for sequence: wanted to do
was 33%
the 16%
but 8%
with 8%
his 8%
* second order. for sequence: to do
with 20%
it 14%
the 4%
something 3%
was 2%
* first order. for word: do
you 20%
it 12%
not 8%
with 6%
that 3%

* third order. for sequence: you will find
that 28%
a 14%
unless 7%
everything 7%
on 7%
* second order. for sequence: will find
that 23%
it 14%
me 9%
a 9%
uses 4%
* first order. for word: find
out 19%
a 8%
the 7%
it 5%
him 4%

从上面的输出可以看出,i can tell 接 you这个单词的概率42%,要比can tell 接 you这个单词的概率20% 要大。显然结果就更精确了。如果调试得当,那么生成的短文结果可能还真看起来像那么回事。

总结

最后,提示一下,first,second,third这三张表可以结合起来使用,third这张表的权重显然更高,依次类推。如果你正在做一个输入法或者聊天提示的功能,可以简单采用这样的方法,先收集用户的输入语句等信息作为训练集合,来构建这几张表,这几张表的优先级最高,优先级最低的是常用的固定成语,词语的搭配,还有流行网络用语等等。

SQLite的并发


title: SQLite的并发
date: 2017-02-23 17:48:40
tags:

  • SQLite
  • 数据库

以前没有好好看过SQLite,并没有对它有更深入点的了解,就当软件本地的配置文件和小规模数据统计来使用,对它的一些知识不是很清晰,今天就借助SQLite官方文档选择性的了解下,主要关注并发,多线程和多进程访问

多个进程应用实例是否能同时访问单个数据库文件?

多进程能同时打开一个数据库,多进程也能同时对一个数据库进行SELECT,但是任意一时刻,有且仅有一个进程能修改一个数据库。

SQLite使用读写锁来控制数据库的访问。但是,使用时请注意:这种锁机制可能在网络文件系统(NFS)上不能正确工作。所以,应该避免多进程访问一个在网络文件系统上的数据库文件。另外,在Windows下,这个锁机制如果在不运行Share.exe守护进程的情况下,在FAT文件系统下就无法工作,由于锁机制,数据库文件在Windows网络共享中多进程访问有严重BUG,所以不要在多台Windows机器之间共享数据库文件。

应该明白,没有一个为嵌入式设计的数据库会支持高并发的,SQLIte允许多进程同时打开,读取数据。但是多进程写数据库下,当任意一个进程想写的话,那么在进程修改数据库期间SQLite会锁住整个数据库文件。但是这样一般只会耗费几毫秒。其他进程只用简单等待锁释放就可以进行操作了。

如果有高并发的需求,一定选择C/S模型的数据库(MySQL,SQLserver,PostgreSQL)。

但是,从经验上讲,大多数应用程序不需要这么大的并发。

当SQLite试图访问一个被其他进程锁住的数据库文件,那么SQLite默认行为会返回SQLITE_BUSY。

SQLite是否线程安全?

是的,如果要让SQLite支持线程安全,就使用SQLITE_THREADSAFE预编译宏来编译SQlite。当然,不用担心,Windows和Linux的二进制发布版就是这样编译的。

SQLite支持线程安全是为了用互斥量同步公共的数据结构。然而要请求和释放锁,所以SQLite性能会略有下降。如果开发者没有线程安全这样的需求,可以重新编译SQLite,关闭互斥量以得到最大性能。

实际上加锁不会太影响性能,影响性能的是锁争用,如果不是多线程,我觉得也可以单线程程序使用线程安全版本的SQLite。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.