死磕以太坊源码分析之EVM如何调用ABI编码的外部方法

配合以下代码进行阅读:https://github.com/blockchainGuide/

写文不易,给个小关注,有什么问题可以指出,便于大家交流学习。

前言

abi是什么?

前面我们认识到的是智能合约直接在EVM上的表示方式,但是,比如我想用java端程序去访问智能合约的某个方法,难道让java开发人员琢磨透汇编和二进制的表示,再去对接?

这明显是不可能的,为此abi产生了。这是一个通用可读的json格式的数据,任何别的客户端开发人员或者别的以太坊节点只要指定要调用的方法,通过abi将其解析为字节码并传递给evm,evm来计算处理该字节码并返回结果给前端。abi就起到这么一个作用,类似于传统的客户端和服务器端地址好交互规则,比如json格式的数据,然后进行交互。

在本系列的上一篇文章中我们看到了Solidity是如何在EVM存储器中表示复杂数据结构的。但是如果无法交互,数据就是没有意义的。智能合约就是数据和外界的中间体。

在这篇文章中我们将会看到SolidityEVM可以让外部程序来调用合约的方法并改变它的状态。

“外部程序”不限于DApp/JavaScript。任何可以使用HTTP RPC与以太坊节点通信的程序,都可以通过创建一个交易与部署在区块链上的任何合约进行交互。

创建一个交易就像发送一个HTTP请求。Web的服务器会接收你的HTTP请求,然后改变数据库。交易会被网络接收,底层的区块链会扩展到包含改变的状态。

交易对于智能合约就像HTTP请求对于Web服务器。

合约交易

让我们来看一下将状态变量设置在0x1位置上的交易。我们想要交互的合约有一个对变量a的设置者和获取者:

pragma solidity ^0.4.11;
contract C {
uint256 a;
function setA(uint256 _a) {
a = _a;
}
function getA() returns(uint256) {
return a;
}
}

这个合约部署在Rinkeby测试网上。可以随意使用Etherscan,并搜索地址 0x62650ae5…进行查看。

我创建了一个可以调用setA(1)的交易,可以在地址0x7db471e5…上查看该交易。

交易的输出数据是:

0xee919d500000000000000000000000000000000000000000000000000000000000000001

对于EVM而言,这只是36字节的元数据。它对元数据不会进行处理,会直接将元数据作为calldata传递给智能合约。如果智能合约是个Solidity程序,那么它会将这些输入字节解释为方法调用,并为setA(1)执行适当的汇编代码。

输入数据可以分成两个子部分:

# 方法选择器(4字节)
0xee919d5
#第一个参数(32字节)
00000000000000000000000000000000000000000000000000000000000000001

前面的4个字节是方法选择器,剩下的输入数据是方法的参数,32个字节的块。在这个例子中,只有一个参数,值是0x1

方法选择器是方法签名的 kecccak256 哈希值。在这个例子中方法的签名是setA(uint256),也就是方法名称和参数的类型。

让我们用Python来计算方法选择器。首先,哈希方法签名:


# 安装pyethereum [https://github.com/ethereum/pyethereum/#installation](https://github.com/ethereum/pyethereum/#installation)> from ethereum.utils import sha3> sha3("setA(uint256)").hex()'ee919d50445cd9f463621849366a537968fe1ce096894b0d0c001528383d4769'

然后获取哈希值的前4字节:

> sha3("setA(uint256)")[0:4].hex()
'ee919d50'

应用二进制接口(ABI)

对于EVM而言,交易的输入数据(calldata)只是一个字节序列。EVM内部不支持调用方法。

智能合约可以选择通过以结构化的方式处理输入数据来模拟方法调用,就像前面所说的那样。

如果EVM上的所有语言都同意相同的方式解释输入数据,那么它们就可以很容易进行交互。 合约应用二进制接口(ABI)指定了一个通用的编码模式。

我们已经看到了ABI是如何编码一个简单的方法调用,例如SetA(1)。在后面章节中我们将会看到方法调用和更复杂的参数是如何编码的。

调用一个获取者

如果你调用的方法改变了状态,那么整个网络必须要同意。这就需要有交易,并消耗gas。

一个获取者如getA()不会改变任何东西。我们可以将方法调用发送到本地的以太坊节点,而不用请求整个网络来执行计算。一个eth_callRPC请求可以允许你在本地模拟交易。这对于只读方法或gas使用评估比较有帮助。

一个eth_call就像一个缓存的HTTP GET请求。

  • 它不改变全球的共识状态
  • 本地区块链(“缓存”)可能会有点稍微过时

制作一个eth_call来调用 getA方法,通过返回值来获取状态a。首先,计算方法选择器:

>>> sha3("getA()")[0:4].hex()
'd46300fd'

由于没有参数,输入数据就只有方法选择器了。我们可以发送一个eth_call请求给任意的以太坊节点。对于这个例子,我们依然将请求发送给 infura.io的公共以太坊节点:

$ curl -X POST \-H "Content-Type: application/json" \"[https://rinkeby.infura.io/YOUR_INFURA_TOKEN](https://rinkeby.infura.io/YOUR_INFURA_TOKEN)" \--data '{"jsonrpc": "2.0","id": 1,"method": "eth_call","params": [{"to": "0x62650ae5c5777d1660cc17fcd4f48f6a66b9a4c2","data": "0xd46300fd"},"latest"]}'

根据ABI,该字节应该会解释为0x1数值。

外部方法调用的汇编

现在来看看编译的合约是如何处理源输入数据的,并以此来制作一个方法调用。思考一个定义了setA(uint256)的合约:

pragma solidity ^0.4.11;
contract C {
uint256 a;
// 注意: `payable` 让汇编简单一点点
function setA(uint256 _a) payable {
a = _a;
}
}

编译:

solc --bin --asm --optimize call.sol

调用方法的汇编代码在合约内部,在sub_0标签下:

sub_0: assembly {
mstore(0x40, 0x60)
and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff)
0xee919d50
dup2
eq
tag_2
jumpi
tag_1:
0x0
dup1
revert
tag_2:
tag_3
calldataload(0x4)
jump(tag_4)
tag_3:
stop
tag_4:
/* "call.sol":95:96 a */
0x0
/* "call.sol":95:101 a = _a */
dup2
swap1
sstore
tag_5:
pop
jump // 跳出
auxdata: 0xa165627a7a7230582016353b5ec133c89560dea787de20e25e96284d67a632e9df74dd981cc4db7a0a0029
}

这里有两个样板代码与此讨论是无关的,但是仅供参考:

  • 最上面的mstore(0x40, 0x60)为sha3哈希保留了内存中的前64个字节。不管合约是否需要,这个都会存在的。
  • 最下面的auxdata用来验证发布的源码与部署的字节码是否相同的。这个是可选择的,但是嵌入到了编译器中

将剩下的汇编代码分成两个部分,这样容易分析一点:

  • 匹配选择器并跳掉方法处
  • 加载参数、执行方法,并从方法返回

首先,匹配选择器的注释汇编代码:

// 加载前4个字节作为方法选择器
and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff)
// 如果选择器匹配`0xee919d50`, 跳转到 setA
0xee919d50
dup2
eq
tag_2
jumpi
// 匹配失败,返回并还原
tag_1:
0x0
dup1
revert
// setA函数
tag_2:
...

除了开始从调用数据里面加载4字节时的位转移,其他的都是非常清晰明朗的。为了清晰可见,给出了汇编逻辑的低级伪代码:

methodSelector = calldata[0:4]
if methodSelector == "0xee919d50":
goto tag_2 // 跳转到setA
else:
// 匹配失败,返回并还原
revert

实际方法调用的注释汇编代码:

// setA
tag_2:
// 方法调用之后跳转的地方
tag_3
// 加载第一个参数(数值0x1).
calldataload(0x4)
// 执行方法
jump(tag_4)
tag_4:
// sstore(0x0, 0x1)
0x0
dup2
swap1
sstore
tag_5:
pop
//程序的结尾,将会跳转到 tag_3并停止
jump
tag_3:
// 程序结尾
stop

在进入方法体之前,汇编代码做了两件事情:

  1. 保存了一个位置,方法调用之后返回此位置
  2. 从调用数据里面加载参数到栈中

低级的伪代码:

// 保存位置,方法调用结束后返回此位置
@returnTo = tag_3
tag_2: // setA
// 从调用数据里面加载参数到栈中
@arg1 = calldata[4:4+32]
tag_4: // a = _a
sstore(0x0, @arg1)
tag_5 // 返回
jump(@returnTo)
tag_3:
stop

将这两部分组合起来:

methodSelector = calldata[0:4]
if methodSelector == "0xee919d50":
goto tag_2 // goto setA
else:
// 无匹配方法。失败
revert
@returnTo = tag_3
tag_2: // setA(uint256 _a)
@arg1 = calldata[4:36]
tag_4: // a = _a
sstore(0x0, @arg1)
tag_5 // 返回
jump(@returnTo)
tag_3:
stop

有趣的小细节:revert的操作码是fd。但是在黄皮书中你不会找到它的详细说明,或者在代码中找到它的实现。实际上,fd不是确实存在的!这是个无效的操作。当EVM遇到了一个无效的操作,它会放弃并且会有还原状态的副作用。

处理多个方法

Solidity编译器是如何为有多个方法的合约产生汇编代码的?

pragma solidity ^0.4.11;
contract C {
uint256 a;
uint256 b;
function setA(uint256 _a) {
a = _a;
}
function setB(uint256 _b) {
b = _b;
}
}

简单,只要一些if-else分支就可以了:

// methodSelector = calldata[0:4]
and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff)
// if methodSelector == 0x9cdcf9b
0x9cdcf9b
dup2
eq
tag_2 // SetB
jumpi
// elsif methodSelector == 0xee919d50
dup1
0xee919d50
eq
tag_3 // SetA
jumpi

伪代码:

methodSelector = calldata[0:4]
if methodSelector == "0x9cdcf9b":
goto tag_2
elsif methodSelector == "0xee919d50":
goto tag_3
else:
// Cannot find a matching method. Fail.
revert

ABI为复杂方法调用进行编码

对于一个方法调用,交易输入数据的前4个字节总是方法选择器。跟在后面的32字节块就是方法参数。 ABI编码规范显示了更加复杂的参数类型是如何被编码的,但是阅读起来非常的痛苦。

另一个学习ABI编码的方式是使用 pyethereum的ABI编码函数 来研究不同数据类型是如何编码的。我们会从简单的例子开始,然后建立更复杂的类型。

首先,导出encode_abi函数:

from ethereum.abi import encode_abi

对于一个有3个uint256类型参数的方法(例如foo(uint256 a, uint256 b, uint256 c)),编码参数只是简单的依次对uint256数值进行编码:

# 第一个数组列出了参数的类型
# 第二个数组列出了参数的值
> encode_abi(["uint256", "uint256", "uint256"],[1, 2, 3]).hex()
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003

小于32字节的类型会被填充到32字节:

> encode_abi(["int8", "uint32", "uint64"],[1, 2, 3]).hex()
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003

对于定长数组,元素还是32字节的块(如果必要的话会填充0),依次排列:

> encode_abi(
["int8[3]", "int256[3]"],
[[1, 2, 3], [4, 5, 6]]
).hex()
// int8[3]. Zero-padded to 32 bytes.
0000000000000000000000000000000000000000000000000000000000000001
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000003
// int256[3].
0000000000000000000000000000000000000000000000000000000000000004
0000000000000000000000000000000000000000000000000000000000000005
0000000000000000000000000000000000000000000000000000000000000006

ABI为动态数组编码

ABI介绍了一种间接的编码动态数组的方法,遵循一个叫做头尾编码的模式。

该模式其实就是动态数组的元素被打包到交易的调用数据尾部,参数(“头”)会被引用到调用数据里,这里就是数组元素。

如果我们调用的方法有3个动态数组,参数的编码就会像这样(添加注释和换行为了更加的清晰):

> encode_abi(
["uint256[]", "uint256[]", "uint256[]"],
[[0xa1, 0xa2, 0xa3], [0xb1, 0xb2, 0xb3], [0xc1, 0xc2, 0xc3]]
).hex()
/************* HEAD (32*3 bytes) *************/
// 参数1: 数组数据在0x60位置
0000000000000000000000000000000000000000000000000000000000000060
// 参数2:数组数据在0xe0位置
00000000000000000000000000000000000000000000000000000000000000e0
// 参数3: 数组数据在0x160位置
0000000000000000000000000000000000000000000000000000000000000160
/************* TAIL (128**3 bytes) *************/
// 0x60位置。参数1的数据
// 长度后跟这元素
0000000000000000000000000000000000000000000000000000000000000003
00000000000000000000000000000000000000000000000000000000000000a1
00000000000000000000000000000000000000000000000000000000000000a2
00000000000000000000000000000000000000000000000000000000000000a3
// 0xe0位置。参数2的数据
0000000000000000000000000000000000000000000000000000000000000003
00000000000000000000000000000000000000000000000000000000000000b1
00000000000000000000000000000000000000000000000000000000000000b2
00000000000000000000000000000000000000000000000000000000000000b3
//0x160位置。参数3的数据
0000000000000000000000000000000000000000000000000000000000000003
00000000000000000000000000000000000000000000000000000000000000c1
00000000000000000000000000000000000000000000000000000000000000c2
00000000000000000000000000000000000000000000000000000000000000c3

HEAD部分有32字节参数,指出TAIL部分的位置,TAIL部分包含了3个动态数组的实际数据。

举个例子,第一个参数是0x60,指出调用数据的第96个(0x60)字节。如果你看一下第96个字节,它是数组的开始地方。前32字节是长度,后面跟着的是3个元素。

混合动态和静态参数是可能的。这里有个(staticdynamicstatic)参数。静态参数按原样编码,而第二个动态数组的数据放到了尾部:

> encode_abi(
["uint256", "uint256[]", "uint256"],
[0xaaaa, [0xb1, 0xb2, 0xb3], 0xbbbb]
).hex()
/************* HEAD (32*3 bytes) *************/
// 参数1: 0xaaaa
000000000000000000000000000000000000000000000000000000000000aaaa
// 参数2:数组数据在0x60位置
0000000000000000000000000000000000000000000000000000000000000060
// 参数3: 0xbbbb
000000000000000000000000000000000000000000000000000000000000bbbb
/************* TAIL (128 bytes) *************/
// 0x60位置。参数2的数据
0000000000000000000000000000000000000000000000000000000000000003
00000000000000000000000000000000000000000000000000000000000000b1
00000000000000000000000000000000000000000000000000000000000000b2
00000000000000000000000000000000000000000000000000000000000000b3

编码字节数组

字符串和字节数组同样是头尾编码。唯一的区别是字节数组会被紧密的打包成一个32字节的块,就像:

> encode_abi(
["string", "string", "string"],
["aaaa", "bbbb", "cccc"]
).hex()
// 参数1: 字符串数据在0x60位置
0000000000000000000000000000000000000000000000000000000000000060
// 参数2:字符串数据在0xa0位置
00000000000000000000000000000000000000000000000000000000000000a0
// 参数3:字符串数据在0xe0位置
00000000000000000000000000000000000000000000000000000000000000e0
// 0x60 (96)。 参数1的数据
0000000000000000000000000000000000000000000000000000000000000004
6161616100000000000000000000000000000000000000000000000000000000
// 0xa0 (160)。参数2的数据
0000000000000000000000000000000000000000000000000000000000000004
6262626200000000000000000000000000000000000000000000000000000000
// 0xe0 (224)。参数3的数据
0000000000000000000000000000000000000000000000000000000000000004
6363636300000000000000000000000000000000000000000000000000000000

对于每个字符串/字节数组,前面的32字节是编码长度,后面跟着才是字符串/字节数组的内容。

如果字符串大于32字节,那么多个32字节块就会被使用:

// 编码字符串的48字节
ethereum.abi.encode_abi(
["string"],
["a" * (32+16)]
).hex() 0000000000000000000000000000000000000000000000000000000000000020
//字符串的长度为0x30 (48)
0000000000000000000000000000000000000000000000000000000000000030
6161616161616161616161616161616161616161616161616161616161616161
6161616161616161616161616161616100000000000000000000000000000000

嵌套数组

嵌套数组中每个嵌套有一个间接寻址。

> encode_abi(
["uint256[][]"],
[[[0xa1, 0xa2, 0xa3], [0xb1, 0xb2, 0xb3], [0xc1, 0xc2, 0xc3]]]
).hex()
//参数1:外层数组在0x20位置上
0000000000000000000000000000000000000000000000000000000000000020
// 0x20。每个元素都是里层数组的位置
0000000000000000000000000000000000000000000000000000000000000003
0000000000000000000000000000000000000000000000000000000000000060
00000000000000000000000000000000000000000000000000000000000000e0
0000000000000000000000000000000000000000000000000000000000000160
// array[0]在0x60位置上
0000000000000000000000000000000000000000000000000000000000000003
00000000000000000000000000000000000000000000000000000000000000a1
00000000000000000000000000000000000000000000000000000000000000a2
00000000000000000000000000000000000000000000000000000000000000a3
// array[1] 在0xe0位置上
0000000000000000000000000000000000000000000000000000000000000003
00000000000000000000000000000000000000000000000000000000000000b1
00000000000000000000000000000000000000000000000000000000000000b2
00000000000000000000000000000000000000000000000000000000000000b3
// array[2]在0x160位置上
0000000000000000000000000000000000000000000000000000000000000003
00000000000000000000000000000000000000000000000000000000000000c1
00000000000000000000000000000000000000000000000000000000000000c2
00000000000000000000000000000000000000000000000000000000000000c3

Gas成本和ABI编码设计

为什么ABI将方法选择器截断到4个字节?如果我们不使用sha256的整个32字节,会不会不幸的碰到不同方法发生冲突的情况? 如果这个截断是为了节省成本,那么为什么在用更多的0来进行填充时,而仅仅只为了节省方法选择器中的28字节而截断呢?

这种设计看起来互相矛盾……直到我们考虑到一个交易的gas成本。

  • 每笔交易需要支付 21000 gas
  • 每笔交易的0字节或代码需要支付 4 gas
  • 每笔交易的非0字节或代码需要支付 68 gas

啊哈!0要便宜17倍,0填充现在看起来没有那么不合理了。

方法选择器是一个加密哈希值,是个伪随机。一个随机的字符串倾向于拥有很多的非0字节,因为每个字节只有0.3%(1/255)的概率是0。

  • 0x1填充到32字节成本是192 gas

    4*31 (0字节) + 68 (1个非0字节)
  • sha256可能有32个非0字节,成本大概2176 gas

    32 * 68
  • sha256截断到4字节,成本大概272 gas

    32*4

ABI展示了另外一个底层设计的奇特例子,通过gas成本结构进行激励。

负整数….

一般使用叫做 补码的方式来表达负整数。int8类型-1的数值编码会都是1。1111 1111

ABI用1来填充负整数,所以-1会被填充为:

ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

越大的负整数(-1大于-2)1越多,会花费相当多的gas。

总结

与智能合约交互,你需要发送原始字节。它会进行一些计算,可能会改变自己的状态,然后会返回给你原始字节。方法调用实际上不存在,这是ABI创造的集体假象。

ABI被指定为一个低级格式,但是在功能上更像一个跨语言RPC框架的序列化格式。

我们可以在DApp和Web App的架构层面之间进行类比:

  • 区块链就是一个备份数据库
  • 合约就像web服务器
  • 交易就像请求
  • ABI是数据交换格式,就像Protocol Buffer

翻译自 https://medium.com/@hayeah/diving-into-the-ethereum-vm-part-2-storage-layout-bc5349cb11b7

死磕以太坊源码分析之EVM如何调用ABI编码的外部方法的更多相关文章

  1. 死磕以太坊源码分析之EVM指令集

    死磕以太坊源码分析之EVM指令集 配合以下代码进行阅读:https://github.com/blockchainGuide/ 写文不易,给个小关注,有什么问题可以指出,便于大家交流学习. 以下指令集 ...

  2. 死磕以太坊源码分析之EVM固定长度数据类型表示

    死磕以太坊源码分析之EVM固定长度数据类型表示 配合以下代码进行阅读:https://github.com/blockchainGuide/ 写文不易,给个小关注,有什么问题可以指出,便于大家交流学习 ...

  3. 死磕以太坊源码分析之EVM动态数据类型

    死磕以太坊源码分析之EVM动态数据类型 配合以下代码进行阅读:https://github.com/blockchainGuide/ 写文不易,给个小关注,有什么问题可以指出,便于大家交流学习. So ...

  4. 死磕以太坊源码分析之Kademlia算法

    死磕以太坊源码分析之Kademlia算法 KAD 算法概述 Kademlia是一种点对点分布式哈希表(DHT),它在容易出错的环境中也具有可证明的一致性和性能.使用一种基于异或指标的拓扑结构来路由查询 ...

  5. 死磕以太坊源码分析之p2p节点发现

    死磕以太坊源码分析之p2p节点发现 在阅读节点发现源码之前必须要理解kadmilia算法,可以参考:KAD算法详解. 节点发现概述 节点发现,使本地节点得知其他节点的信息,进而加入到p2p网络中. 以 ...

  6. 死磕以太坊源码分析之rlpx协议

    死磕以太坊源码分析之rlpx协议 本文主要参考自eth官方文档:rlpx协议 符号 X || Y:表示X和Y的串联 X ^ Y: X和Y按位异或 X[:N]:X的前N个字节 [X, Y, Z, ... ...

  7. 死磕以太坊源码分析之Fetcher同步

    死磕以太坊源码分析之Fetcher同步 Fetcher 功能概述 区块数据同步分为被动同步和主动同步: 被动同步是指本地节点收到其他节点的一些广播的消息,然后请求区块信息. 主动同步是指节点主动向其他 ...

  8. 死磕以太坊源码分析之Ethash共识算法

    死磕以太坊源码分析之Ethash共识算法 代码分支:https://github.com/ethereum/go-ethereum/tree/v1.9.9 引言 目前以太坊中有两个共识算法的实现:cl ...

  9. 死磕以太坊源码分析之downloader同步

    死磕以太坊源码分析之downloader同步 需要配合注释代码看:https://github.com/blockchainGuide/ 这篇文章篇幅较长,能看下去的是条汉子,建议收藏 希望读者在阅读 ...

随机推荐

  1. 枚举--让盗版美国总统wcc给你整明白哈哈

    1.为什么要有枚举 Java中的枚举其实是一种语法糖,在 JDK 1.5之后出现,用来表示固定且有限个的对象.比如一个季节类有春.夏.秋.冬四个对象:一个星期有星期一到星期日七个对象.这些明显都是固定 ...

  2. Java 容器系列总结

    为什么要使用集合 当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, 因为我们在实际开发中,存储的数据的类型是多种多样的,于是 ...

  3. 90% 的 Java 程序员都说不上来的为何 Java 代码越执行越快(2)- TLAB预热

    经常听到 Java 性能不如 C/C++ 的言论,也经常听说 Java 程序需要预热,那么其中主要原因是啥呢? 面试的时候谈到 JVM,也有很多面试官喜欢问,为啥 Java 程序越执行越快呢? 一般人 ...

  4. 设计模式(十五)——命令模式(Spring框架的JdbcTemplate源码分析)

    1 智能生活项目需求 看一个具体的需求 1) 我们买了一套智能家电,有照明灯.风扇.冰箱.洗衣机,我们只要在手机上安装 app 就可以控制对这些家电工作. 2) 这些智能家电来自不同的厂家,我们不想针 ...

  5. Codeforces Round #671 (Div. 2)

    比赛链接:https://codeforces.com/contest/1419 A. Digit Game 题意 给出一个 $n$ 位数,游戏规则如下: 1-indexed Raze标记奇数位 Br ...

  6. HDU6191 Query on A Tre【dsu on tree + 01字典树】

    Query on A Tree Problem Description Monkey A lives on a tree, he always plays on this tree. One day, ...

  7. AtCoder Beginner Contest 161

    比赛链接:https://atcoder.jp/contests/abc161/tasks AtCoder Beginner Contest 161 第一次打AtCoder的比赛,因为是日本的网站终于 ...

  8. CodeForces 893C (并查集板子题)

    刷题刷到自闭,写个博客放松一下 题意:n个人,m对朋友,每寻找一个人传播消息需要花费相应的价钱,朋友之间传播消息不需要花钱,问最小的花费 把是朋友的归到一起,求朋友中花钱最少的,将所有最少的加起来. ...

  9. ACdream1414 Geometry Problem

    Problem Description       Peter is studying in the third grade of elementary school. His teacher of ...

  10. 内存耗尽后Redis会发生什么

    前言 作为一台服务器来说,内存并不是无限的,所以总会存在内存耗尽的情况,那么当 Redis 服务器的内存耗尽后,如果继续执行请求命令,Redis 会如何处理呢? 内存回收 使用Redis 服务时,很多 ...