WebAssembly

WebAssembly 系列(一)生动形象地介绍 WebAssembly
WebAssembly 系列(二)JavaScript Just-in-time (JIT) 工作原理
WebAssembly 系列(三)编译器如何生成汇编
WebAssembly 系列(四)WebAssembly 工作原理
WebAssembly 系列(五)为什么 WebAssembly 更快?
WebAssembly 系列(六)WebAssembly 的现在与未来

如何评论浏览器最新的 WebAssembly 字节码技术?
WebAssembly:解决 JavaScript 痼疾的银弹?
WebAssembly让你的Javascript计算性能提升70%

现在的JavaScript代码要进行性能优化,通常使用一些常规手段,如:延迟执行、预处理、setTimeout等异步方式避免处理主线程,高大上一点的会使用WebWorker。即使对于WebWorker也仅仅是解决了阻塞主线程的问题,但是对于JavaScript计算性能慢的问题并没有解决。在目前阶段,WebAssembly 适合大量密集计算、并且无需频繁与 JavaScript 及 DOM 进行数据通讯的场景。比如游戏渲染引擎、物理引擎、图像音频视频处理编辑、加密算法等。

WebAssembly是一种运行在现代网络浏览器中的新型代码并且提供新的性能特性和效果。它设计的目的不是为了手写代码而是为诸如C、C++和Rust等低级源语言提供一个高效的编译目标。WebAssembly的模块可以被导入的到一个网络app(或Node.js)中,并且暴露出供JavaScript使用的WebAssembly函数。JavaScript框架不但可以使用WebAssembly获得巨大性能优势和新特性,而且还能使得各种功能保持对网络开发者的易用性。简单来说WebAssembly就是浏览器提供的一项直接运行二进制机器代码的能力。这些机器代码怎么来呢,是通过C、C++或Rust等语言编译来的。

一、JIT(Just-in-time compilation)

JavaScript 于 1995 年问世,它的设计初衷并不是为了执行起来快,在前 10 个年头,它的执行速度也确实不快。紧接着,浏览器市场竞争开始激烈起来。被人们广为传播的“性能大战”在 2008 年打响。许多浏览器引入了 Just-in-time 编译器,也叫 JIT。基于 JIT 的模式,JavaScript 代码的运行渐渐变快。正是由于这些 JIT 的引入,使得 JavaScript 的性能达到了一个转折点,JS 代码执行速度快了 10 倍。

在代码的世界中,通常有两种方式来翻译机器语言:解释器和编译器。如果是通过解释器,翻译是一行行地边解释边执行。编译器是把源代码整个编译成目标代码,执行时不再需要编译器,直接在支持目标代码的平台上运行。这两种翻译的方式都各有利弊。

1.解释器的利弊
解释器启动和执行的更快。你不需要等待整个编译过程完成就可以运行你的代码。从第一行开始翻译,就可以依次继续执行了。正是因为这个原因,解释器看起来更加适合 JavaScript。对于一个 Web 开发人员来讲,能够快速执行代码并看到结果是非常重要的。这就是为什么最开始的浏览器都是用 JavaScript 解释器的原因。
可是当你运行同样的代码一次以上的时候,解释器的弊处就显现出来了。比如你执行一个循环,那解释器就不得不一次又一次的进行翻译,这是一种效率低下的表现。

2.编译器的利弊
编译器的问题则恰好相反。
它需要花一些时间对整个源代码进行编译,然后生成目标文件才能在机器上执行。对于有循环的代码执行的很快,因为它不需要重复的去翻译每一次循环。
另外一个不同是,编译器可以用更多的时间对代码进行优化,以使的代码执行的更快。而解释器是在 runtime 时进行这一步骤的,这就决定了它不可能在翻译的时候用很多时间进行优化。

3.JIT综合了两者的优点
为了解决解释器的低效问题,后来的浏览器把编译器也引入进来,形成混合模式。不同的浏览器实现这一功能的方式不同,不过其基本思想是一致的。在 JavaScript 引擎中增加一个监视器(也叫分析器)。监视器监控着代码的运行情况,记录代码一共运行了多少次、如何运行的等信息。如果同一行代码运行了几次,这个代码段就被标记成了 “warm”,如果运行了很多次,则被标记成 “hot”。

4.基线编译器
如果一段代码变成了 “warm”,那么 JIT 就把它送到编译器去编译,并且把编译结果存储起来。代码段的每一行都会被编译成一个“桩”(stub),同时给这个桩分配一个以“行号 + 变量类型”的索引。如果监视器监视到了执行同样的代码和同样的变量类型,那么就直接把这个已编译的版本 push 出来给浏览器。

5.优化编译器
如果一个代码段变得 “very hot”,监视器会把它发送到优化编译器中。生成一个更快速和高效的代码版本出来,并且存储之。

6.去优化
整个优化器起作用的链条是这样的,监视器从他所监视代码的执行情况做出自己的判断,接下来把它所整理的信息传递给优化器进行优化。如果某个循环中先前每次迭代的对象都有相同的形状,那么就可以认为它以后迭代的对象的形状都是相同的。可是对于 JavaScript 从来就没有保证这么一说,前 99 个对象保持着形状,可能第 100 个就少了某个属性。
正是由于这样的情况,所以编译代码需要在运行之前检查其假设是不是合理的。如果合理,那么优化的编译代码会运行,如果不合理,那么 JIT 会认为做了一个错误的假设,并且把优化代码丢掉。这时(发生优化代码丢弃的情况)执行过程将会回到解释器或者基线编译器,这一过程叫做去优化。

通常优化编译器会使得代码变得更快,但是一些情况也会引起一些意想不到的性能问题。如果你的代码一直陷入优化<->去优化的怪圈,那么程序执行将会变慢,还不如基线编译器快。大多数的浏览器都做了限制,当优化/去优化循环发生的时候会尝试跳出这种循环。比如,如果 JIT 做了 10 次以上的优化并且又丢弃的操作,那么就不继续尝试去优化这段代码了桩。

7.总结
简而言之 JIT 是什么呢?它是使 JavaScript 运行更快的一种手段,通过监视代码的运行状态,把 hot 代码(重复执行多次的代码)进行优化。通过这种方式,可以使 JavaScript 应用的性能提升很多倍。为了使执行速度变快,JIT 会增加很多多余的开销,这些开销包括:

  • 优化和去优化开销
  • 监视器记录信息对内存的开销
  • 发生去优化情况时恢复信息的记录对内存的开销
  • 对基线版本和优化后版本记录的内存开销

下面这张图片介绍了性能使用的大概分布情况。


image.png

图中的每一个颜色条都代表了不同的任务:

  • Parsing——表示把源代码变成解释器可以运行的代码所花的时间;
  • Compiling + optimizing——表示基线编译器和优化编译器花的时间。一些优化编译器的工作并不在主线程运行,不包含在这里。
  • Re-optimizing——当 JIT 发现优化假设错误,丢弃优化代码所花的时间。包括重优化的时间、抛弃并返回到基线编译器的时间。
  • Execution——执行代码的时间
  • Garbage collection——垃圾回收,清理内存的时间

这里注意:这些任务并不是离散执行的,或者按固定顺序依次执行的。而是交叉执行,比如正在进行解析过程时,其他一些代码正在运行,而另一些正在编译。

二、asm.js

JIT 基于运行期分析编译,而 Javascript 是一个没有类型的语言,于是, 大部分时间,JIT 编译器其实是在猜测 Javascript 中的类型。既然 JIT 遇到的问题是类型不确定问题和有一些语言功能,比如异常,for in , JIT 起来很麻烦, 我可不可以搞个方法让用户不去用这些功能,同时让他们把用的类型都标注出来啊。按照这个思路, 催生了两种实现路径:

一种是 Typescript, Dart, JSX 为代表的,基本思想是, 我搞个其他的语言,这个语言是强类型的,所以程序猿们需要指定类型,然后我把它编译成 Javacript 不就行了嘛。强类型的语言编译成弱类型还不容易,什么,不知道怎么编? 把类型去掉就行了嘛。

另一种是火狐的 Asm.js 为代表的, 做一个 javascript 子集, 同时试图利用标注的方法,加上变量类型。这是 Mozilla 在 2013 年推出的一项新技术,舍弃了大量会导致性能问题的语法,并且被设计为通过 C / C++ 代码编译生成,而非手工编写 asm.js 代码。上述的 sum 函数在 asm.js 中表现为:

function sum ( a ,b ) {
    a = a | 0;
    b = b | 0;
    return ( a + b ) | 0;
}

上述代码中,标准的 JavaScript 引擎会对其进行解析,并生成正确的结果,而 asm.js 会根据一些不会对运行时造成计算结果错误的特殊标识对变量的类型进行声明(比如 a = a | 0 表示变量 a 是一个整数),通过这种方式,这种代码既可以在支持 asm.js 的 JavaScript 引擎上得到很高的性能,也会在不支持的设备上继续按照正确的逻辑进行执行,而非无法运行。虽然如此,asm.js 仍然存在着一些问题,主要是基于 JavaScript 语法的文本格式解析速度不够快,并且代码尺寸偏大。

如果你没有注意到,第二种的速度提升潜力比第一种要大非常多。因为第一种,无论如何,也是就是让JIT (即时编译) 快一点, 第二种那可直接就编译了啊 (AOT).

Web Assembly 就是第二种方式,说到底,Mozilla, Google, Microsoft, and Apple 觉得 Asm.js 这个方法有前途,想标准化一下,大家都能用。有了大佬们的支持,Web Assembly 比 asm.js 要激进很多。 Web Assembly 连标注 Js 这种事情都懒得做了,不是要 AOT 吗? 我直接给字节码好不好?(后来改成 AST 树)。对于不支持 Web Assembly 的浏览器, 会有一段 Javascript 把 Web Assembly 重新翻译为 Javascript 运行, 这个技术叫 polyfill, HTML5 刚出来的时候很常用的一个技术。使用 AST 的原因是因为 AST 比字节码更容易压缩,也更容易翻译。

image.png

上图展示了如何通过编写 C / C++ 代码生成 WebAssembly 内容。

首先通过 LLVM ,将 C/C++ 源代码编译为 LLVM bytecode。这 是一种跨语言的底层虚拟机字节码,理论上所有强类型编程语言均可以生成这种字节码。通过这一点可以得知,在未来理论上所有强类型编程语言(诸如 Java / C# 等)均可以开发 WebAssembly 程序。

其次,通过 EMScripten 中的后端编译器,将这种抽象字节码生成 asm.js 格式的文件。这是一种特殊的 JavaScript 代码,部分 JavaScript 引擎会将这种格式以比通常的 JavaScript 代码更快的速度运行,并且由于 asm.js 仍然是 JavaScript,所以哪怕 JavaScript 引擎不支持该特性,也会以通常的方式运行这段逻辑。这意味着使用 C/C++ 编写的源代码,哪怕用户设备不支持 WebAssembly,也可以回退到 JavaScript 运行并得到一致的结果。

第三,asm.js 会通过另一个编译器生成为 WebAssembly 的 .wasm 文件,由于 WebAssembly 是二进制格式,相比 JavaScript 而言,其代码体积同比小很多,并且由于已经是面向机器码的格式,也无需在运行前对源代码耗费时间进行 JIT 编译操作。

通过上述内容可以看出,WebAssembly 理论上可以通过任何强类型语言生成,不强制依赖用户的本地运行环境,代码体积小、解析速度快,几乎是 Web 开发未来的一颗“银色子弹”。

三、WebAssembly

机器码的翻译并不是只有一种,不同的机器有不同的机器码,就像我们人类也说各种各样的语言一样,机器也“说”不同的语言。人类和外星人之间的语言翻译,可能会从英语、德语或中文翻译到外星语 A 或者外星语 B。而在程序的世界里,则是从 C、C++ 或者 JAVA 翻译到 x86 或者 ARM。
你想要从任意一个高级语言翻译到众多汇编语言中的一种(依赖机器内部结构),其中一种方式是创建不同的翻译器来完成各种高级语言到汇编的映射。


image.png

这种翻译的效率实在太低了。为了解决这个问题,大多数编译器都会在中间多加一层。它会把高级语言翻译到一个低层,而这个低层又没有低到机器码这个层级。这就是中间代码( intermediate representation,IR)。


image.png

这就是说编译器会把高级语言翻译到 IR 语言,而编译器另外的部分再把 IR 语言编译成特定目标结构的可执行代码。
重新总结一下:编译器的前端把高级语言翻译到 IR,编译器的后端把 IR 翻译成目标机器的汇编代码。

那么在上图中,WebAssembly 在什么位置呢?实际上,你可以把它看成另一种“目标汇编语言”。WebAssembly 与其他的汇编语言不一样,它不依赖于具体的物理机器。可以抽象地理解成它是概念机器的机器语言,而不是实际的物理机器的机器语言。正因为如此,WebAssembly 指令有时也被称为虚拟指令。它比 JavaScript 代码更直接地映射到机器码,它也代表了“如何能在通用的硬件上更有效地执行代码”的一种理念。所以它并不直接映射成特定硬件的机器码。


image.png

目前对于 WebAssembly 支持情况最好的编译器工具链是 LLVM。有很多不同的前端和后端插件可以用在 LLVM 上。还有一个易用的工具,叫做 Emscripten。它通过自己的后端先把代码转换成自己的中间代码(叫做 asm.js),然后再转化成 WebAssembly。实际上它背后也是使用的 LLVM。Emscripten 还包含了许多额外的工具和库来包容整个 C/C++ 代码库,所以它更像是一个软件开发者工具包(SDK)而不是编译器。例如系统开发者需要文件系统以对文件进行读写,Emscripten 就有一个 IndexedDB 来模拟文件系统。不考虑太多的这些工具链,只要知道最终生成了 .wasm 文件就可以了。

四、为什么 WebAssembly 更快

1.文件获取
这一步并没有显示在图表中,但是这看似简单地从服务器获取文件这个步骤,却会花费很长时间。WebAssembly 比 JavaScript 的压缩率更高,所以文件获取也更快。即便通过压缩算法可以显著地减小 JavaScript 的包大小,但是压缩后的 WebAssembly 的二进制代码依然更小。这就是说在服务器和客户端之间传输文件更快,尤其在网络不好的情况下。
2.解析
当到达浏览器时,JavaScript 源代码就被解析成了抽象语法树。浏览器采用懒加载的方式进行,只解析真正需要的部分,而对于浏览器暂时不需要的函数只保留它的桩(stub,译者注:关于桩的解释可以在之前的文章中有提及)。而 WebAssembly 则不需要这种转换,因为它本身就是中间代码。它要做的只是解码并且检查确认代码没有错误就可以了。

3.编译和优化
在关于 JIT 的文章中,我有介绍过,JavaScript是在代码的执行阶段编译的。因为它是弱类型语言,当变量类型发生变化时,同样的代码会被编译成不同版本。不同浏览器处理 WebAssembly 的编译过程也不同,有些浏览器只对 WebAssembly 做基线编译,而另一些浏览器用 JIT 来编译。不论哪种方式,WebAssembly 都更贴近机器码,所以它更快,使它更快的原因有几个:

  • 在编译优化代码之前,它不需要提前运行代码以知道变量都是什么类型。
  • 编译器不需要对同样的代码做不同版本的编译。
  • 很多优化在 LLVM 阶段就已经做完了,所以在编译和优化的时候没有太多的优化需要做。

4.重优化
有些情况下,JIT 会反复地进行“抛弃优化代码<->重优化”过程。当 JIT在优化假设阶段做的假设,执行阶段发现是不正确的时候,就会发生这种情况。比如当循环中发现本次循环所使用的变量类型和上次循环的类型不一样,或者原型链中插入了新的函数,都会使 JIT 抛弃已优化的代码。
反优化过程有两部分开销。第一,需要花时间丢掉已优化的代码并且回到基线版本。第二,如果函数依旧频繁被调用,JIT 可能会再次把它发送到优化编译器,又做一次优化编译,这是在做无用功。在 WebAssembly 中,类型都是确定了的,所以 JIT 不需要根据变量的类型做优化假设。也就是说 WebAssembly 没有重优化阶段。

5.执行
自己也可以写出执行效率很高的 JavaScript 代码。你需要了解 JIT 的优化机制,例如你要知道什么样的代码编译器会对其进行特殊处理(JIT 文章里面有提到过)。然而大多数的开发者是不知道 JIT 内部的实现机制的。即使开发者知道 JIT 的内部机制,也很难写出符合 JIT 标准的代码,因为人们通常为了代码可读性更好而使用的编码模式,恰恰不合适编译器对代码的优化。加之 JIT 会针对不同的浏览器做不同的优化,所以对于一个浏览器优化的比较好,很可能在另外一个浏览器上执行效率就比较差。

正是因为这样,执行 WebAssembly 通常会比较快,很多 JIT 为 JavaScript 所做的优化在 WebAssembly 并不需要。另外,WebAssembly 就是为了编译器而设计的,开发人员不直接对其进行编程,这样就使得 WebAssembly 专注于提供更加理想的指令(执行效率更高的指令)给机器就好了。执行效率方面,不同的代码功能有不同的效果,一般来讲执行效率会提高 10% - 800%。

6.垃圾回收
JavaScript 中,开发者不需要手动清理内存中不用的变量。JS 引擎会自动地做这件事情,这个过程叫做垃圾回收。可是,当你想要实现性能可控,垃圾回收可能就是个问题了。垃圾回收器会自动开始,这是不受你控制的,所以很有可能它会在一个不合适的时机启动。目前的大多数浏览器已经能给垃圾回收安排一个合理的启动时间,不过这还是会增加代码执行的开销。
目前为止,WebAssembly 不支持垃圾回收。内存操作都是手动控制的(像 C、C++一样)。这对于开发者来讲确实增加了些开发成本,不过这也使代码的执行效率更高。

五、WebAssembly 优缺点

1.中间调用的开销
目前,在 JS 中调用 WebAssembly 的速度比本应达到的速度要慢。这是因为中间需要做一次“蹦床运动”。JIT 没有办法直接处理 WebAssembly,所以 JIT 要先把 WebAssembly 函数发送到懂它的地方。这一过程是引擎中比较慢的地方。如果你传递的是单一任务给 WebAssembly 模块,那么不用担心这个开销,因为只有一次转换,也会比较快。但是如果是频繁地从 WebAssembly 和 JavaScript 之间切换,那么这个开销就必须要考虑了。

2.快速加载
JIT 必须要在快速加载和快速执行之间做权衡。如果在编译和优化阶段花了大量的时间,那么执行的必然会很快,但是启动会比较慢。目前有大量的工作正在研究,如何使预编译时间和程序真正执行时间两者平衡。

3.无法直接操作 DOM
目前 WebAssembly 没有任何方法可以与 DOM 直接交互。就是说你还不能通过比如element.innerHTML 的方法来更新节点。想要操作 DOM,必须要通过 JS。
目前 WebAssembly 类似 WebWorker ,只能进行单纯的数值计算工作,不能在 C++ 层直接操作 DOM 节点。虽然在未来路线图中提及这一特性会在后续加入,但是在目前阶段 WebAssembly 更适合被用于更纯粹的密集型数据计算工作,而非直接编写业务逻辑。WebAssembly 更适合编写应用程序中对性能要求比较高的库,并与 JavaScript 编写的业务逻辑进行通讯,并在 JavaScript 端对 DOM 节点进行操作。

以笔者最近开发的白鹭引擎 5.0 的渲染库为例,白鹭引擎对外提供 JavaScript API,开发者编写的 JavaScript 逻辑代码会汇总为一组命令队列发送给 WebAssembly 层,然后 WebAssembly 负责所有的计算工作,最终生成一组基于 WebGL 格式的数据流,最后 JavaScript 对这组数据流进行简单的解析并直接调用 DOM 的 WebGL 接口传递数据。

Egret Engine5.0可以直接将 H5 游戏代码编译成机器码运行,对比Egret Engine4.0版效率提升可达300%。Egret Engine5.0团队进行封闭开发期间,精心研磨着重重写了引擎底层从而支持 WebAssembly技术,为开发者提供更好的性能。如果浏览器不支持 WebAssembly ,5.0版引擎能够自动智能切换成正常 JavaScript 版本,开发者无需担忧正常使用。


image.png

4.共享内存的并发性
提升代码执行速度的一个方法是使代码并行运行,不过有时也会适得其反,因为不同的线程在同步的时候可能会花费更多的时间。

5.调试不方便
目前在浏览器中调试 WebAssembly 就像调试汇编一样,很少的开发者可以手动地把自己的源代码和汇编代码对应起来。我们在致力于开发出更加适合开发者调试源代码的工具。

WebAssembly 被设计为了一种开放的、可调试的程序,但目前无论是 Chrome 还是 FireFox ,在调试方面还有很大的提升空间。由于在目前阶段调试较为困难,所以用 WebAssembly 编写业务逻辑代码对研发来说还是很不方便的。

6.无法处理垃圾回收
如果你能提前确定变量类型,那就可以把你的代码变成 WebAssembly,例如 TypeScript 代码就可以编译成 WebAssembly。但是现在的问题是 WebAssembly 没办法处理垃圾回收的问题,WebAssembly 中的内存操作都是手动的。所以 WebAssembly 会考虑提供方便的 GC 功能,以方便开发者使用。

7.体积很小
代码体积很小,我们将大约 300k 左右(压缩后)JavaScript 逻辑改用 WebAssembly 重写后,体积仅有 90k 左右。虽然使用 WebAssembly 需要引入一个 50k-100k 的 JavaScript 类库作为基础设施,但是总体来看资源尺寸的优势还是很大的

8.安全性有很大改善
由于代码格式是二进制、无法直接在浏览器中看到源码,尽管理论上仍然可以通过逆向工程一定程度上得到原有的业务逻辑,但是由于开发者可以在编译时使用了 -O3 等激进的优化策略,所以最终反编译得到的业务逻辑也是很难阅读的。虽然理论上一切在客户端的内容都是不安全的,但是与所有代码都直接暴露给用户相比,代码安全性得到了很大的改善

9.大量反复执行的优化不明显,而普通业务逻辑又不便大量使用
在运行 benchmark 等极限测试时,游戏引擎使用 WebAssembly 并不比 JavaScript 有几何量级的提升。笔者的推论是:由于 JavaScript 引擎的 JIT 机制会把经常运行的函数进行极限的编译优化,所以在 benchmark 这种代码大量反复执行的测试环境下,无论是 JavaScript 版本,还是 WebAssembly 版本,运行的都是高度优化后的机器码,虽然 WebAssembly 版本仍然比 JavaScript 版有一定的性能优势,但是并不明显。

在运行业务逻辑代码时,由于大部分业务逻辑代码只运行一次,所以 JavaScript 引擎只会对这部分代码进行简单的编译优化而非极限优化,所以运行这一部分代码 WebAssembly 相比 JavaScript 版本而言提升巨大,但是因为上文所述,不建议开发者在编写业务逻辑时使用 WebAssembly,所以这里陷入了一个两难。在目前而言,理想情况是除了底层库之外,部分关键的涉及性能问题的逻辑也可以使用 WebAssembly 进行编写。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容