罗列的博客

这是一个罗列发呆的地方

0%

Long time Pythoneer Tim Peters succinctly channels the BDFL’s guiding principles for Python’s design into 20 aphorisms, only 19 of which have been written down.


  1. Beautiful is better than ugly.
  2. Explicit is better than implicit.
  3. Simple is better than complex.
  4. Complex is better than complicated.
  5. Flat is better than nested.
  6. Sparse is better than dense.
  7. Readability counts.
  8. Special cases aren’t special enough to break the rules.
  9. Although practicality beats purity.
  10. Errors should never pass silently.
  11. Unless explicitly silenced.
  12. In the face of ambiguity, refuse the temptation to guess.
  13. There should be one– and preferably only one –obvious way to do it.
  14. Although that way may not be obvious at first unless you’re Dutch.
  15. Now is better than never.
  16. Although never is often better than right now.
  17. If the implementation is hard to explain, it’s a bad idea.
  18. If the implementation is easy to explain, it may be a good idea.
  19. Namespaces are one honking great idea – let’s do more of those!

MESI

处理器上有一套完整的协议,来保证 Cache 一致性。比较经典的 Cache 一致性协议当属 MESI 协议,奔腾处理器有使用它,很多其他的处理器都是使用它的变种。

单核 Cache 中每个 Cache line 有 2 个标志:dirty 和 valid 标志,它们很好的描述了 Cache 和 Memory(内存)之间的数据关系(数据是否有效,数据是否被修改),而在多核处理器中,多个核会共享一些数据,MESI 协议就包含了描述共享的状态。

在 MESI 协议中,每个 Cache line 有 4 个状态,可用 2 个 bit 表示,它们分别是:

状态 描述
M(Modified) 这行数据有效,数据被修改了,和内存中的数据不一致,数据只存在于本 Cache 中。
E(Exclusive) 这行数据有效,数据和内存中的数据一致,数据只存在于本 Cache 中。
S(Shared) 这行数据有效,数据和内存中的数据一致,数据存在于很多 Cache 中。
I(Invalid) 这行数据无效。

M(Modified)和 E(Exclusive)状态的 Cache line,数据是独有的,不同点在于 M 状态的数据是 dirty 的(和内存的不一致),E 状态的数据是 clean 的(和内存的一致)。

S(Shared)状态的 Cache line,数据和其他 Core 的 Cache 共享。只有 clean 的数据才能被多个 Cache 共享。

I(Invalid)表示这个 Cache line 无效。

当内核需要访问的数据不在本 Cache 中,而其它 Cache 有这份数据的备份时,本 Cache 既可以从内存中导入数据,也可以从其它 Cache 中导入数据,不同的处理器会有不同的选择。

MESI 协议为了使自己更加通用,没有定义这些细节,只定义了状态之间的迁移,下面的描述假设本 Cache 从内存中导入数据。

Invalid:

事件 行为 next state
local Read 如果其它 Cache 没有这份数据,本 Cache 从内存中取数据,Cache line 状态变成 E;如果其它 Cache 有这份数据,且状态为 M,则将数据更新到内存,本 Cache 再从内存中取数据,2 个 Cache 的 Cache line 状态都变成 S;如果其它 Cache 有这份数据,且状态为 S 或者 E,本 Cache 从内存中取数据,这些 Cache 的 Cache line 状态都变成 S E/S
Local Write 从内存中取数据,在 Cache 中修改,状态变成 M;如果其它 Cache 有这份数据,且状态为 M,则要先将数据更新到内存;如果其它 Cache 有这份数据,则其它 Cache 的 Cache line 状态变成 I M
Remote Read 既然是 Invalid,别的核的操作与它无关 I
Remote Write 既然是 Invalid,别的核的操作与它无关 I

Exclusive:

事件 行为 next state
Local Read 从 Cache 中取数据,状态不变 E
Local Write 修改 Cache 中的数据,状态变成 M M
Remote Read 数据和其它核共用,状态变成了 S S
Remote Write 数据被修改,本 Cache line 不能再使用,状态变成 I I

Shared:

事件 行为 next state
Local Read 从 Cache 中取数据,状态不变 S
Local Write 修改 Cache 中的数据,状态变成 M,其它核共享的 Cache line 状态变成 I M
Remote Read 状态不变 S
Remote Write 数据被修改,本 Cache line 不能再使用,状态变成 I I

Modified:

事件 行为 next state
Local Read 从 Cache 中取数据,状态不变 M
Local Write 修改 Cache 中的数据,状态不变 M
Remote Read 这行数据被写到内存中,使其它核能使用到最新的数据,状态变成 S S
Remote Write 这行数据被写到内存中,使其它核能使用到最新的数据,由于其它核会修改这行数据,状态变成 I I

AMD 的 Opteron 处理器使用从 MESI 中演化出的 MOESI 协议,O(Owned)是 MESI 中 S 和 M 的一个合体,表示本 Cache line 被修改,和内存中的数据不一致,不过其它的核可以有这份数据的拷贝,状态为 S。

Intel 的 core i7 处理器使用从 MESI 中演化出的 MESIF 协议,F(Forward)从 Share 中演化而来,一个 Cache line 如果是 Forward 状态,它可以把数据直接传给其它内核的 Cache,而 Share 则不能。

DMA 本来不属于 CPU 体系架构部分的内容,只因为在开发中经常要用到其相关的知识,所以这里就其基本概念、工作原理、常见问题做一个总结。

概述

DMA 的英文拼写是“Direct Memory Access”,汉语的意思就是直接内存访问。

DMA 既可以指内存和外设直接存取数据这种内存访问的计算机技术,又可以指实现该技术的硬件模块(对于通用计算机 PC 而言,DMA 控制逻辑由 CPU 和 DMA 控制接口逻辑芯片共同组成,嵌入式系统的 DMA 控制器内建在处理器芯片内部,一般称为 DMA 控制器,DMAC)。

DMA 是一个控制器!

DMA 内存访问

使用 DMA 的好处就是它不需要 CPU 的干预而直接服务外设,这样 CPU 就可以去处理别的事务,从而提高系统的效率。

对于慢速设备,如 UART,其作用只是降低 CPU 的使用率,但对于高速设备,如硬盘,它不只是降低 CPU 的使用率,而且能大大提高硬件设备的吞吐量。

因为对于这种设备,CPU 直接供应数据的速度太低,因 CPU 只能一个总线周期最多存取一次总线,而且对于 ARM,它不能把内存中 A 地址的值直接搬到 B 地址。它只能先把 A 地址的值搬到一个寄存器,然后再从这个寄存器搬到 B 地址。也就是说,对于 ARM,要花费两个总线周期才能将 A 地址的值送到 B 地址。

而 DMA 就不同了,一般系统中的 DMA 都有突发(Burst)传输的能力,在这种模式下,DMA 能一次传输几个甚至几十个字节的数据,所以使用 DMA 能使设备的吞吐能力大为增强。

考虑到虚拟内存技术,注意这几个点:

  • DMA 使用物理地址,程序是使用虚拟地址的,所以配置 DMA 时必须将虚拟地址转化成物理地址。
  • 因为程序使用虚拟地址,而且一般使用 cache 地址,所以 Cache 中的内容与其物理地址(内存)的内容不一定一致,所以在启动 DMA 传输前一定要将该地址的 cache 刷新,即写入内存。
  • OS 并不能保证每次分配到的内存空间在物理上是连续的。尤其是在系统使用过一段时间而又分配了一块比较大的内存时。所以每次都需要判断地址是不是连续的,如果不连续就需要把这段内存分成几段让 DMA 完成传输

嵌入式设备的 DMA

直接存储器存取(DMA)控制器是一种在系统内部转移数据的独特外设,可以将其视为一种能够通过一组专用总线将内部和外部存储器与每个具有 DMA 能力的外设连接起来的控制器。它之所以属于外设,是因为它是在处理器的编程控制下来执行传输的。值得注意的是,通常只有数据流量较大(kBps 或者更高)的外设才需要支持 DMA 能力,这些应用方面典型的例子包括视频、音频和网络接口。

一般而言,DMA 控制器将包括一条地址总线、一条数据总线和控制寄存器。高效率的 DMA 控制器将具有访问其所需要的任意资源的能力,而无须处理器本身的介入,它必须能产生中断。最后,它必须能在控制器内部计算出地址。

一个处理器可以包含多个 DMA 控制器。每个控制器有多个 DMA 通道,以及多条直接与存储器站(memory bank)和外设连接的总线,如图所示。在很多高性能处理器中集成了两种类型的 DMA 控制器。

第一类通常称为“系统 DMA 控制器”,可以实现对任何资源(外设和存储器)的访问,对于这种类型的控制器来说,信号周期数是以系统时钟(SCLK)来计数的,以 ADI 的 Blackfin 处理器为例,频率最高可达 133MHz。

第二类称为内部存储器 DMA 控制器(IMDMA),专门用于内部存储器所处位置之间的相互存取操作。因为存取都发生在内部(L1-L1、L1-L2,或者 L2-L2),周期数的计数则以内核时钟(CCLK)为基准来进行,该时钟的速度可以超过 600MHz。

每个 DMA 控制器有一组 FIFO,起到 DMA 子系统和外设或存储器之间的缓冲器的作用。对于 MemDMA(Memory DMA)来说,传输的源端和目标端都有一组 FIFO 存在。当资源紧张而不能完成数据传输的话,则 FIFO 可以提供数据的暂存区,从而提高性能。

因为通常会在代码初始化过程中对 DMA 控制器进行配置,内核就只需要在数据传输完成后对中断做出响应即可。你可以对 DMA 控制进行编程,让其与内核并行地移动数据,而同时让内核执行其基本的处理任务―那些应该让它专注完成的工作。

在一个优化的应用中,内核永远不用参与任何数据的移动,而仅仅对 L1 存储器中的数据进行读写。于是,内核不需要等待数据的到来,因为 DMA 引擎会在内核准备读取数据之前将数据准备好。图 2 给出了处理器和 DMA 控制器间的交互关系。

由处理器完成的操作步骤包括:建立传输,启用中断,生成中断时执行代码。返回到处理器的中断输入可以用来指示“数据已经准备好,可进行处理”。

数据除了往来外设之外,还需要从一个存储器空间转移到另一个空间中。例如,视频源可以从一个 视频端口直接流入 L3 存储器,因为工作缓冲区规模太大,无法放入到存储器中。我们并不希望让处理器在每次需要执行计算时都从外部存储读取像素信息,因此为 了提高存取的效率,可以用一个存储器到存储器的 DMA(MemDMA)来将像素转移到 L1 或者 L2 存储器中。

到目前为止,我们还仅专注于数据的移动,但是 DMA 的传送能力并不总是用来移动数据。

在最简单的 MemDMA 情况中,我们需要告诉 DMA 控制器源端地址、目标端地址和待传送的字的个数。每次传输的字的大小可以是 8、16 或者 12 位。 我们只需要改变数据传输每次的数据大小,就可以简单地增加 DMA 的灵活性。例如,采用非单一大小的传输方式时,我们以传输数据块的大小的倍数来作为地址增量。也就是说,若规定 32 位的传输和 4 个采样的跨度,则每次传输结束后,地址的增量为 16 字节(4 个 32 位字)。

参考文献

https://nieyong.github.io/wiki_cpu/CPU%E4%BD%93%E7%B3%BB%E6%9E%B6%E6%9E%84-DMA.html

概述

macOS 默认使用 LLDB 来进行 C/C++ 程序的调试, LLDB 能够逐行调试程序,使开发者能够了解程序的变量值以及堆栈是如何变化的,一旦学会之后使用起来也比 printf 更加方便和简单,赶紧学起来吧。

LLDB 实现原理

在此之前,请考虑如何实现一个能够监听其他程序(被监听者称为 Client)运行情况的程序(监听者称为 Server)。

第一种方式是 Server 拷贝 Client 的代码来模拟 Client 运行,并且在运行的过程中,Server 通过在模拟过程中使用额外的指令从而能够查看和修改 Client 的运行堆栈和数据信息,其中,Valgrind 就是这样实现的。这种方式的优点是无需预先编译 Client 程序,缺点是因为需要运行额外的指令所以 Server 的运行会比 Client 慢很多(Valgrind 大概会会原程序慢 20-50 倍)。

第二种方式是使用操作系统的 ptrace 系统调用,这也是 LLDB 的实现方式。ptrace 系统调用可以让 A 进程监听和控制 B 进程的内存和寄存器。ptrace 系统调用有以下几个主要功能:

  • 捕获 exec 系统调用并阻止程序的运行。
  • 查询 CPU 的寄存器来获取当前的指令,数据和栈地址。
  • 监听 clone/fork 事件来判断是否创建新的线程。
  • 读取或者修改 Client 内存变量。
  • 也就是说利用 ptrace 系统调用,Client 运行的每一行代码的情况 Server 都能知道。

常用指令

这里我们先简单列出来 LLDB 的常见指令,接下来的例子会介绍如何使用(其中括号中的为指令的缩写,例如 break main 可以缩写为 b main):

1
2
3
4
5
6
7
breakpoint set (b) - 设置断点,也就是程序暂停的地方
run (r) - 启动目标程序,如果遇到断点则暂停
step (s) - 进入下一条指令中的函数内部
backtrace (bt) - 显示当前的有效函数
frame (f) - 默认显示当前栈的内容,可以通过 `frame arg` 进入特定的 frame(用作输出本地变量)
next (n) - 运行当前箭头指向行
continue (c) - 继续运行程序直到遇到断点。

示例

C 标准库中的 strlen 函数的作用是找到字符串 s 的长度,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

size_t strlen(const char *s) {
const char *sc;

for (sc = s; *sc != '\0'; ++sc)
/* nothing */;
return sc - s;
}

int main() {
// 创建 str 字符串
char str[] = "Hello World";
// 调用 strlen 函数,并把值赋予 length
int length = strlen(str);
// 在终端打印内容
printf("The length of str is %d\n", length);
return 0;
}

如果你不熟悉 C/C++ 的话,可能不太理解 strlen 函数的实现方式,这时候就是 LLDB 大显身手的时候了,使用 LLDB 调试以下程序之前,有几个步骤:

  1. 把上面的例子保存为 test.c
  2. 在终端运行 gcc test.c -g -o test (这里的 -g 参数保证 LLDB 显示的是源代码而不是汇编代码)
  3. 终端运行 lldb test,这是告诉 LLDB 要调试哪个程序,没有问题的话,终端会输出:/path $ lldb test (lldb) target create "test" Current executable set to 'test' (x86_64).
    4, 运行程序: 这时候 LLDB 已经在监听 test 程序了,test 的一举一动都逃不过 LLDB 的法眼。最基础的命令是 run,这条指令会开始运行 test 程序。终端会输出:
1
2
3
1. Process 9782 launched: '/path/test' (x86_64)
2. The length of str is 11
3. Process 9782 exited with status = 0 (0x00000000)

这里,第一行标示了进程的 ID,第二行是 test 程序的输出,也就是 str 字符串的长度。最后的是程序的返回值,在这里 0 则为正常结束。当然,像这样仅仅有一个输出和返回值对我们调试没有什么帮助。因为程序运行得太快一下子就结束了,我们还没有来得及理解这个程序。LLDB 对于 printf 的优点在于可以逐步调试,我们可以选择一行行地运行程序,然后输出我们需要的堆栈信息以及变量值。让我们重新开始,我们先使用 Control + C 退出 LLDB 重新运行 lldb test ,然后运行 break main,这句指令代表我们在 main 函数的开头打上断点,(break 11 也能得到相同的结果,这里 11 是 main 的行号)代表让 test 程序在运行到 main 函数的时候暂停,这时候再次运行 run, 程序就会在 12 行停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
(lldb) breakpoint set main
Breakpoint 1: where = test`main + 33 at test.c:12:10, address = 0x0000000100000f01
(lldb) run
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x0000000100000f01 test`main at test.c:12:10
9 }
10
11 int main() {
-> 12 char str[] = "Hello World";
13 int length = strlen(str);
14 printf("The length of str is %d\n", length);
15 return 0;
Target 0: (test) stopped.

那么 breakpoint 指令是怎么实现的呢?为什么可以让程序在特定的地方暂停呢?简单来说:

  1. breakpoint set 指令 会在参数所在地写入一个无效的地址值,在例子中,则是 main 函数。
  2. 因为地址无效,所以 test 程序运行出错,抛出异常,系统会传送 SIGTRAP 信号给 LLDB。
  3. LLDB 这时候可以查看需要的堆栈信息或者变量值。
  4. LLDB 把正确的下一条指令重新写入到 test 程序中。

箭头指向的 12 行是下一条要执行的指令,这时候 str 还没进行定义,使用 print 指令来验证。

1
2
(lldb) print *str
(char) $0 = '\0'

要运行 12 行 的代码,我们试试 next 指令:

1
2
3
4
5
6
7
8
9
10
(lldb) next
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x0000000100000f15 test`main at test.c:13:18
10
11 int main() {
12 char str[] = "Hello World";
-> 13 int length = strlen(str);
14 printf("The length of str is %d\n", length);
15 return 0;
16 }

再次查看 str 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(lldb) p *str
(char) $1 = 'H'
# 这时候 str 已经被定义了,指向了 'H',这也是我们预料之中。frame variable 用作列出当前所有的变量值。

(lldb) frame variable
(char [12]) str = "Hello World"
(int) length = 0
# 如果要修改某个变量的值,可以使用 expr

(lldb) expr *str = 'A'
(char) $2 = 'A'
# 再次查看
(lldb) frame variable
(char [12]) str = "Aello World"
(int) length = 0

使用 expr 之后,str 的值已经变成 “Aello World” 了。下一行要运行的代码是 13 行,这个表达式包含了一个函数调用,当运行 13 行的时候,strlen 函数会被压到 test 程序的栈顶,如下图。

使用 step 进入函数内部(如果使用 next 的话我们会运行到 14 行,这时候 strlen 函数已经执行完毕了)。

1
2
3
4
5
6
7
8
9
10
11
12
(lldb) **step**
Process 14972 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
frame #0: 0x0000000100000e98 test`strlen(s="Aello World") at test.c:6:15
3 size_t strlen(const char *s) {
4 const char *sc;
5
-> 6 for (sc = s; *sc != '\0'; ++sc)
7 /* nothing */;
8 return sc - s;
9 }
Target 0: (test) stopped.

看到箭头指向的是 6 行,我们已经进入到 strlen 函数内部了。使用 backtrace 来显示当前的有效函数信息,可以看到我们当前 fram #0 也就是当前在 strlen。

1
2
3
4
5
(lldb) backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
* frame #0: 0x0000000100000e98 test`strlen(s="Aello World") at test.c:6:15
frame #1: 0x0000000100000f1a test`main at test.c:13:18
frame #2: 0x00007fff6d7e77fd libdyld.dylib`start + 1

从 6 行的代码我们可以看到程序一直在 for 循环中运行,每次运行都会判断 sc 是否已经到达 s 字符串的结尾,如果是则停止 for 循环,strlen 函数最后返回 sc 和 s 的距离。strlen 函数结束后,返回值被赋予到 main 函数的 length 中。继续运行 next 命令,箭头指向 14 行,

(lldb) n
Process 15186 stopped

  • thread #1, queue = ‘com.apple.main-thread’, stop reason = step over
    frame #0: 0x0000000100000f1f test`main at test.c:14:41
    11 int main() {
    12 char str[] = “Hello World”;
    13 int length = strlen(str);
    -> 14 printf(“The length of str is %d\n”, length);
    15 return 0;
    16 }
    这时候 length 的值已经更新了,我们可以通过 frame variable 来验证:

(char [12]) str = “Aello World”
(int) length = 11
再次使用 next 命令,终端输出了 The length of str is 11,这也是我们想要的结果。

总结
LLDB 的基本用法已经介绍结束了,虽然 LLDB 的命令非常多,不过关键的就是 break, next, step, frame, 这几个,更多的使用例子可以参考官方文档:https://lldb.llvm.org/use/map.h

引言

总结一下罗列的 2020 年

刚好是罗列学生生涯的最后一年,有一个从学生到打工人身份的转变

所以先吐槽一下关于找工作的事情吧。

工作

今年一开始的时候并没有想好要找具体哪方面的工作,做软件,做硬件?还是机器学习,数据分析?包括当数学老师我都认真考虑过(甚至还去面试了)。

原因嘛,一个是因为在学校的时候,什么工作都做过一些,工作领域的选择还挺多的。

另一个也是因为我的家乡是一个煤矿城市,在小时候因为国家还在工业化建设,那个时候煤矿钢铁都很兴盛,我从小到大也是看着城市繁荣起来。

但是16年国家开始去产能,煤炭钢铁首当其冲,相关企业经济一下子就近乎崩溃,以前大家都觉得很好的“矿务局”和“钢铁公司”,现在工资都发不出来了,而那些新进来的大学生也都只能离职后寻找别的出路,或者和企业一起度过夕阳。

见证了这些的我在找工作初期非常畏首畏尾,也就犹犹豫豫什么工作都在找。

当然啦我后面倒是想通了,这个可以以后再给大家分享。

然后在具体面试的时候,因为今年确实是不好就业,很多公司都在缩招,甚至很多大厂秋招都没有名额了,面试只不过是为了完成kpi。

完成kpi就会遇见很多一言难尽的面试官,包括一些大厂的,出了一个算法题,我明明做对了,他也说不出来我哪里错了,却还是说我没做对的面试官(我保证事后我又多次验证过无数次都没问题)。

当然这个也遇见过很多很好的面试官,包括给我分享他自己的从业经历和对未来方向的理解,还有我有些做的不好的地方给我解释和指导,这些都让我非常感动,也非常感激。

实习

然后是这学期做了一些自己很久之前就想做的事情。

这学期我还找了前端实习的工作,这也算是一个意外之喜吧。

是一个新兴的做区块链做公链的公司

前端 conflux

保险

重疾

理财

基金 数字货币

生活

时隔一年膝盖还是没好

学会了自由泳

和同学出去自驾,自己一个人开车去云南

手术

烦恼

我觉得自己丧失了开心的能力

闪兑 / 自动化做市场的进化和分类(代码demo)

从英文翻译而来-Uniswap是一个去中心化的加密货币交易所,它通过使用智能合约促进以太坊区块链上的加密货币令牌之间的自动化交易。截至2020年10月,按每日交易量计算,Uniswap估计是最大的分散交易所和第四大加密货币交易所

DeFi (Decentralized Finance)

基于智能合约构建的

  1. 加密资产
  2. 金融类智能合约
  3. 金融类协议

目前主要都在以太坊网络实现

2020年,区块链技术在金融领域取得巨大进展,DeFi大爆发。

  • 借贷
  • 衍生品
  • 保险
  • 支付平台

DeFi 的意义

传统金融是中心化托管。

但是信息核实成本越来越大,导致效率越来越低。

借助智能合约,未来可以实现更多的金融形式

DeFi 的赛道

基于在金融活动中承担的角色不同,DeFi 有很多赛道

三个主要赛道为:

  1. 去中心化借贷

通过中心化的协议, 提供借入借出服务。

  1. 预言机

为(1)中心化借贷提供基准价格,参与借贷用户可以基于预言机价格借入借出,避免借贷价格偏离市场正常价格

  1. 去中心化交易所 (DEX)

不需要注册既可以交易的交易所,比中心化交易所匿名性更强,但是流动性更弱。

Uniswap

Uniswap 是一个在以太坊区块链上运行的交易所,它支援 ETH与 Token 之间、Token 与 Token 之间的快速兑换。

开发团队没做 ICO、不抽手续费、也不收上币费。

历史

Uniswap 的设计最早可以追溯到 Vitalik 在 2016 年 10 月在 Reddit 发的

Let’s run on-chain decentralized exchanges the way we run prediction markets

这篇文章描述了一个在区块链上运作的去中心化交易所的雏形,

白皮书写的特色

  1. ease-of-use(易用性)

在 Uniswap 交易所上买卖币时,你只要决定好卖出的币种、买入什么币、买或卖的数量是多少,按下 Swap 送出交易,在交易上链后就能立即取得你应得的币。

而且就算是 Token A 换 Token B,在 Uniswap 也只要发出一笔交易就能完成兑换,在其它交易所中可能需要发两笔交易(第一笔将 Token A 换成某种中介货币(如 ETH, DAI)再发第二笔交易换成 Token B)才能完成。

注:实际上在 Uniswap 也是将 Token A 换成 ETH 再换成 Token B,只是它让这两个动作发生在同一笔交易里。

  1. gas efficiency(gas 使用效率)

根据白皮书中的资料,Uniswap 交易消耗的 gas 量是以太坊上的几家主流交易所之中最低的,也就代表在 Uniswap 交易要付的矿工费最少。

这主要得益于它相对简单的做市机制:

Uniswap 不是采用挂单搓合机制来完成交易,而是根据合约中储备的资金量算出当下的交易价格,并立刻从资金池中取出对应的金额传给使用者,整体的运算量相对较少。

  1. censorship resistance(抗审查性)

抗审查性体现在 Uniswap 上架新币的门槛,就是没有门槛,任何使用者都能在 Uniswap 上架任何 Token。

这点即使在众多去中心化交易所之中也是少见的,大多数的去中心化交易所虽然不会像中心化交易所那样向你收取上币费 ,但还是要申请上币、通过审查后,运营团队才会让你的 Token 可以在他们的交易所上交易。(可参考上币规则 of IDEX, KyberSwap, Bancor, EtherDelta)

但在 Uniswap,任何使用者只要发起一个 createExchange 的交易,就能让一个 Token 上架到 Uniswap 的交易对中,上架后也没有人能迫使它下架。

  1. zero rent extraction(零抽租)

在 Uniswap 的合约设计中,没有人有任何特权,开发团队也不从交易中抽取费用。但这不代表在 Uniswap 上交易是没有手续费的。

要让交易被打包进以太坊区块链就要付 gas fee,这笔钱跟交易的金额大小无关,以近期的币价和网路拥挤程度估计。

什么是流动性提供者(liquidity provider)?

「流动性提供者」是一个金融术语,指的是帮助一个金融市场提高流动性的个体。一个拥有越高流动性(市场深度越大)的交易所,其使用者就越能在短时间内以稳定的价格完成大额资产的交换,使用者的交易体验当然就越好。

反之,在一个流动性不足的交易所,就有可能因为一笔大额的交易导致币价剧烈波动。

在大部分的交易所中都有流动性提供者或做市商(market maker)这样的角色存在,做市商会在买、卖两个方向上挂单,让想要交易的使用者只需要跟做市商的订单搓合就能完成交易,而不需要等待拥有相反需求的交易对手出现,市场流动性就能提高。

在 Uniswap,流动性提供者要做的事情是:替一个 ETH - Token 交易对增加 ETH 与 Token 的储备资金,储备金越多,ETH <=> Token 的交易价格就越稳定,该 Token 的流动性就越高。

但如果「替一个交易对增加流动性」这件事不能带来利益,应该很少人会自愿这么做,所以 Uniswap 的做法是从每一笔 ETH <=> Token 交易中抽取「流动性提供者费用」0.3% 分给流动性提供者们,逐利的人们就会自愿为 Uniswap 增加流动性以赚取被动收入,交易者们也能享受到更好的交易体验,达成双赢。

Uniswap 有什么缺点?

  1. 不能自行决定买卖价格

你只能被动接受 Uniswap 给你的价格,不能挂单在你想要的价格。

  1. 交易费用不低

0.3% 的费用跟其它中心化、去中心化交易所比起来都算是偏高,如果你的目标是尽量以漂亮的价格完成交易,不建议使用 Uniswap。

  1. Front Running(超前交易)

Front Running 在许多去中心化交易所、应用中都是一个问题。

简单地说,从你发出交易到交易上链之前,其他人有机会赶在你的交易完成之前执行另一笔交易,使得你的成交价格偏离预期,你因此吃亏,对方因此得益。

Background

因为罗列最近一学期(研三上)在一家区块链公司实习,现在马上要放寒假了,回家过年时不可避免会被问“什么是区块链”之类的问题,所以打算提前捋一下相关的概念。

每个板块分为两部分

  • 写给自己:只是给我自己看的,用作思考
  • 写给爸妈:把我自己的思考翻译为人话(食用时只需要阅读此部分即可)

概述

区块链大概是个什么呀?

写给自己

其实我也经常会问我自己这个问题,区块链到底是什么?

从数据结构的角度可以理解为区块链就是一个 JSON 串,隔一段时间向后插入一个新的 Block (还是一个 JSON),又用到前一个 Block 的 hash 作为头部数据

这样“链表”的数据结构就构成了区块链,Block 内包括…

但是这只是技术原理,其实很多人更关心的是在区块链技术上能做些什么事儿。

就像是如果我能说出“机器学习”就是你手机每次解锁用的“指纹识别/人脸识别”这样虽然并不准确,还有一些以偏概全,但是能一下子让听者得到他想知道的内容的一个直观感受。

写给爸妈

区块链可以简单总结为,一个记录转信息的系统,但是这个和我们日常生活中的却不太一样。

我们日常生活中一般转账一般是统一交给银行来处理,因为我们相信银行不会记错账,我通过银行转账也确实能转到对方手里。

而区块链记账的原理是,所有人都拿一个小账本,任何一笔交易,比如我转账给你,我们都会拿一个大喇叭告诉所有人,我拿了xxx元给你,并且大家都在他们的记账本上记录一下我们这笔交易。

所以每个人的账本既要记录自己,也要记录别人的交易内容,大家所有人账本上的内容都是一样的,这样做确实好像很麻烦,但是却能带来不少好处,这就是后面的内容。

为什么要设计区块链?

写给自己

区块链的诞生离不开一个词汇,“去中心化”。

就是我们手里的钱会不会贬值完全靠发货币的机构,比如银行来控制,这样有些坏政府会靠着超发货币来满足执政者的需求,后果就是大家拼命赚钱来对抗通货膨胀,但是少数人通过控制货币政策就可以保全自己的资产。这些原因可以总结为货币以及货币的交易是过于中心化的问题(一定程度上少数人可以操控货币规则从中获利)。

为了能保护自己的劳动成果,或者说为了让货币的价值控制在多数人手里,人们一直在寻找一个解决办法,当然寻找解决办法的期间会面临很多问题,比如该用什么样的货币(货币政策)、如何保证交易过程中一系列安全(科学技术问题)、怎么让大众接受这个理念(在这个技术上的生态)。

区块链算是在这套体系中最后一块版图,所以大家会把这一个体系称之为区块链,而大家的目的就是在这套系统上建立一个分布式的货币规则,并且分布式的的特点让这个系统里没有“银行”的存在,也保证了对于每个人都是公平的。

所以人们开始尝试逐渐在这套系统上建立一个和现实世界货币并行的新的货币体系,其中的钱也可以和人民币/美元等按照汇率兑换,当然这个汇率纯粹地尊重市场调整。

写给爸妈

民国末期有过这么一个事儿,政府为了缓解财政压力,就不停的印钱,导致的结果是老百姓手里的钱贬值,物价翻了4、5倍,不知不觉中老百姓手里的钱就不值钱了。

这主要原因是当中心化的货币管理机构存在时,不可避免会出现这类事情。中国政府算是控制的好的,所以我们可能感受不深,但是美国现在都还深受其害。

我看过这么一个说法,说美国总统竞选的时候疯狂的许诺自己上台以后会做的事情,因为他上台之后完全可以通过多印钱给政府来兑现承诺,但是这样破坏货币平衡就可能会导致货币的价值发生波动。

在这么一个背景下,全世界都在思考有没有一个“没有中心”的货币制度,当然这期间会遇见很多难题,包括用什么样的货币,大家怎么交易之类的。总之过了很多年的学者研究,现在全世界尝试在区块链这个技术上构建这么一个货币体系。

区块链怎么交易呢

写给自己

区块链为了保证一直有使用者维护这一套系统(记账),会给每个记账者奖励数字货币,就比如比特币 BTC,而这个数字货币就会在这一套区块链上流通。而大家交易的过程中交易的也就是这些随着大家记账而越来越多的货币(所以记账的过程也称为挖矿)

而具体的交易就和我们日常生活使用钱一样,比如我们买个苹果会用人民币,而如果对方接受我付美元而且我也有美元,那么我也可以付美元,同理如果对方接受区块链货币,那么我也可以付“区块链货币”。

至于我该付多少,区块链货币也会有和人民币的一个汇率,而这个汇率也会市场波动。

写给爸妈

其实就和我们日常交易一样,我们可以在银行按照一定比率把手里的钱换成美元,也可以在银行按照一定比率把手里的钱换成区块链货币,然后大家按照相应的价格交易就好啦,具体方法大概就像我微信给你转账一样吧hhhh。

ps当然不是每个银行都能兑换到区块链货币啊,如果你想兑换我再告诉你在什么银行hhhh。

大家每个人都要记账太累了吧

写给自己

区块链和机器学习近些年来才活跃起来的原因是一样的,叫做算力提升。

全世界能有足够多机器都在维护着这个货币平台(记账),就像是日不落帝国一样,永远有设备,永远有很多很多的设备在帮每一笔交易记账,所以这样一个貌似很麻烦的事情,其实都是机器上的脚本在运行罢了。

写给爸妈

这个肯定不是人记账嘛,就是我们现在电脑手机性能越来越好了,所以大家就会把平时闲置的机器打开那个程序,就一直帮这个系统记账,当然花费的只是电费罢了,人还是该看电视看电视啊。

区块链的现在和未来

虽然区块链现在地位确实有点暧昧,因为不受控制会让政府有一些苦恼,但是现在全世界已经有无数的人在使用这么一套系统了,这个潮流肯定已经阻挡不了了。

而中国人民币在全球不算强势的货币,就像现在大家还是都拿美元作为参照物,所以中国也蛮想在这种数字货币的领域中率先出击,做出一些能让人民币,或许未来的数字人民币能在世界上和美元搏一搏。

说这些只是想给你们我公司不是什么非法组织啦hhhhh

ps. 喝酒时

  • 问:区块链是什么啊?

  • 答:区块链就是帮别人炒股的平台,我们不炒股,我们提供技术支持

附录

这里没涉及到但是我觉得也很重要的内容

  • 怎么保障我自己财产安全?
    • RSA 非对称加密 公钥(地址)/私钥
  • 怎么保障交易安全?
    • 时间戳 + SHA256哈希 + 难度调整
  • 怎么保证不会有通货膨胀?
    • 区块链发币规则都还有自己的经济学原理(黄皮书),通常是通货紧缩来保证货币价格越来越值钱(所以比特币越来越贵)
  • 为什么相信比特币有价值
    • hhh这个问题无法回答,但是现代人类就是只要有一部分相信有价值,那么一样东西就是有价值的呀(而且这个相信的人还会越来越多)(共识)

指针对于一个类型TT*就是指向T的指针类型,也即一个T*类型的变量能够保存一个T对象的地址,而类型T是可以加一些限定词的,如constvolatile等等。

1. 为什么C/C++语言使用指针?

原文链接:https://www.cnblogs.com/gxcdream/p/4805612.html

  1. 一方面,每一种编程语言都使用指针。不止C/C++使用指针。

每一种编程语言都使用指针。C++将指针暴露给了用户(程序员),而Java和C#等语言则将指针隐藏起来了。
“Everything uses pointers. C++ just exposes them rather than hiding them,”
It’s easier to give someone an address to your home than to give a copy of your home to everyone.

  1. 另一方面
    使用指针的优点和必要性:
  • 指针能够有效的表示数据结构;
  • 能动态分配内存,实现内存的自由管理;
  • 能较方便的使用字符串;
  • 便捷高效地使用数组
  • 指针直接与数据的储存地址有关,比如:值传递不如地址传递高效,因为值传递先从实参的地址中取出值,再赋值给形参代入函数计算;而指针则把形参的地址直接指向实参地址,使用时直接取出数据,效率提高,特别在频繁赋值等情况下(注意:形参的改变会影响实参的值!)

引用和指针有什么区别?

本质:引用是别名,指针是地址

具体的:

  1. 从现象上看,指针在运行时可以改变其所指向的值,而引用一旦和某个对象绑定后就不再改变。

这句话可以理解为:指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。

  1. 从内存分配上看,程序为指针变量分配内存区域,而不为引用分配内存区域,因为引用声明时必须初始化,从而指向一个已经存在的对象。引用不能指向空值。

注:标准没有规定引用要不要占用内存,也没有规定引用具体要怎么实现,具体随编译器 http://bbs.csdn.net/topics/320095541

  1. 从编译上看,程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。

指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。
符号表生成后就不会再改,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改。
这是使用指针不安全而使用引用安全的主要原因。从某种意义上来说引用可以被认为是不能改变的指针。

  1. 不存在指向空值的引用这个事实,意味着使用引用的代码效率比使用指针的要高。

因为在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空(野指针)。

  1. 理论上,对于指针的级数没有限制,但是引用只能是一级。如下:
1
2
3
4
int** p1;         // 合法。指向指针的指针
int*& p2; // 合法。指向指针的引用
int&* p3; // 非法。指向引用的指针是非法的
int&& p4; // 非法。指向引用的引用是非法的

注意上述读法是从左到右

特别之处const

为什么要提到const关键字呢?因为const对指针和引用的限定是有差别的:

常量指针 & 常量引用

顶层const (top-level const)

1
const int* pointer = &a
  1. 常量指针:指向常量的指针,在指针定义语句的类型前加const,表示指向的对象是常量。

定义指向常量的指针只限制指针的间接访问操作,而不能规定指针指向的值本身的操作规定性。

常量指针定义const int* pointer = &a告诉编译器,*pointer是常量,不能将*pointer作为左值进行操作。

  1. 常量引用:指向常量的引用,在引用定义语句的类型前加const,表示指向的对象是常量。

也跟指针一样不能对引用指向的变量进行重新赋值操作。

指针常量 & 引用常量

底层const (low-level const)

1
int* const pointer = &a

指针定义语句的指针名前const,表示指针本身是常量。在定义指针常量时必须初始化!

而这是引用与生俱来的属性,无需使用const。

指针常量定义int* const pointer = &b告诉编译器,pointer(地址)是常量,不能作为左值进行操作,但是允许修改间接访问值,即*pointer(地址所指向内存的值)可以修改。

常量指针常量 & 常量引用常量

1
const int* const pointer = &a

告诉编译器,pointer*pointer都是常量,他们都不能作为左值进行操作。

而不存在所谓的”常量引用常量”,因为引用变量就是引用常量。

C++不区分变量的const引用和const变量的引用。程序决不能给引用本身重新赋值,使他指向另一个变量,因此引用总是const的。如果对引用应用关键字const,起作用就是使其目标称为const变量。即

没有:const double const& a = 1;
只有:

1
2
3
4
5
const double& a = 1;
double b = 1;
const double& a = b;
b = 2; // 正确
a = 3; // error: assignment of read-only reference `a`

总结:有一个规则可以很好的区分const是修饰指针,还是修饰指针指向的数据——画一条垂直穿过指针声明的星号(*),如果const
出现在线的左边,指针指向的数据为常量;如果const出现在右边,指针本身为常量。而引用本身就是常量,即不可以改变指向。

指针传递和引用传递

指针传递参数本质上是值传递的方式,它所传递的值是一个地址值。

1
2
3
4
5
int val = 1;
int* a = &val;

fun(*a);
// 此处仍然传递进去的是a的值,不会改变外面的

值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。

值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。

引用传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。

1
2
3
4
5
int val = 1;
int& a = val;

fun(a);
// 此处放进去的a如果改变可以改变外面的a

被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。

正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量。
但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。
而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。
如果想通过指针参数传递来改变主调函数中的相关变量, 那就得使用指向指针的指针,或者指针引用。

从概念上讲

  • 指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。

  • 引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。

参考文献:

http://bbs.csdn.net/topics/80358667
http://www.guokr.com/post/443914/
http://blog.csdn.net/listening_music/article/details/6921608
http://www.tc5u.com/cpp/2400451.htm

gcc

1
2
3
4
5
6
7
8
9
10
11
12
13
[text] C program (p1.c p2.c)
|
| [Compiler] (gcc -Og -S)
V
[text] Asm program (p1.s p2.s)
|
| [Assembler] (gcc / as)
V
[binary] Object program (p1.o p2.o)
|
| [Linker] (gcc / ld)
V {add static libraries .a}
[binary] Executable program (p)
1
2
-S : generate asm file .s (or will generate .out)
-Og: basic optimization

just like this

1
2
3
4
5
6
7
8
// sum.c
long plus(long x, long y);

void sumstore(long x, long y, long *dest)
{
long t = plus(x, y);
*dest = t;
}
1
2
3
4
5
6
7
<!-- sum.s -->
pushq %rbx
movq %rdx, %rbx
call plus@PLT
movq %rax, (%rbx)
popq %rbx
ret

disassemble

1
objdump -d sum.o > sum.d

output to sum.d

reg

1
2
3
4
5
parm1: %rdi
parm2: %rsi
parm3: %rdx

ret: %rax

instruction

1
movzbl: move with zero extension byte to long

for x86-64:
any computation where the result is a 32-bit result. it will add zero to the remaining 32 bits of the register (other just like 16-bit or 8-bit will not)

call func & local variable

caller saved and callee saved

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
%rax: return value

%rdi
%rsi
%rdx
%rcx
%r8
%r9: 6 reg used to passing arguments

%r10
%r11: caller saved temporaries

%rbx
%r12
%r13
%r14: callee must save

%rbp: callee saved used as frame ptr

%rsp: callee save, restored to original value on exit from procedure