下面以交易的字节占用为例,解析btcd是如何处理golang的字节占用问题的。
交易结构
一个交易的大小分成了隔离见证占用和非隔离见证占用,非隔离见证部分包括:交易版本号、交易输入的数量、交易输入本身、交易输出数量、交易输出本身、锁定时间。
交易的结构:
type MsgTx struct {
Version int32
TxIn []*TxIn
TxOut []*TxOut
LockTime uint32
}
一个交易的字节占用可被拆解为:
描述 | 长度(byte) |
---|---|
版本 | 4 |
交易输入数量 | 1+ |
交易输入 | 41+ |
交易输出数量 | 1+ |
交易输出 | 9+ |
锁定时间 | 4 |
```
func (msg *MsgTx) SerializeSize() int {
n := msg.baseSize()
if msg.HasWitness() {
// The marker, and flag fields take up two additional bytes.
n += 2
// Additionally, factor in the serialized size of each of the
// witnesses for each txin.
for _, txin := range msg.TxIn {
n += txin.Witness.SerializeSize()
}
}
return n
}
```
隔离见证数据在交易结构中位于交易输入结构中,但btcd单独计算了其数据占用情况,而没有和TxIn一起计算。
交易的字节占用
一个交易包括4字节版本号、4字节锁定时间、txin长度的变长整数占用、txout长度的变长整数占用,再加上txin和txout占用。
```
func (msg *MsgTx) baseSize() int {
// Version 4 bytes + LockTime 4 bytes + Serialized varint size for the
// number of transaction inputs and outputs.
n := 8 + VarIntSerializeSize(uint64(len(msg.TxIn))) + VarIntSerializeSize(uint64(len(msg.TxOut)))
for _, txIn := range msg.TxIn {
n += txIn.SerializeSize()
}
for _, txOut := range msg.TxOut {
n += txOut.SerializeSize()
}
return n
}
```
VarIntSerializeSize()函数对不同大小的整数规范了不同的字节占用,也是为了优化存储,以及网络传输成本。具体内容可以参考: Variable length integer
```
func VarIntSerializeSize(val uint64) int {
// The value is small enough to be represented by itself, so it's
// just 1 byte.
if val < 0xfd {
return 1
}
// Discriminant 1 byte plus 2 bytes for the uint16.
if val <= math.MaxUint16 {
return 3
}
// Discriminant 1 byte plus 4 bytes for the uint32.
if val <= math.MaxUint32 {
return 5
}
// Discriminant 1 byte plus 8 bytes for the uint64.
return 9
}
```
代码理解:
- 传递一个uint64类型的整数参数
- 如果比0xfd小,那么就返回1,说明数据在一个字节内存储而不会溢出
- 如果比0xfd大,在分别和math.MaxUint16、math.MaxUint32比较,也就是2字节和4字节能容纳的最大正整数,如果在相应的范围,则返回对应的字节数量。需要注意的是2字节、4字节判断前需要分别有0xFD、0xFE标记,故返回的数据需要加1
- 如果前面的分支都没有进入,由于变长整数的最大值被规范在uin64范围内,因此该数据占用为8个字节,加上开始的1字节0xFF标记,故返回9
txin的字节占用了:包括32字节的outpoint hash、4字节的outpoint索引、4字节的序列号,总共40字节。再加上sigScript长度的变长整数占用,以及sigScript占用字节数。
```
func (t *TxIn) SerializeSize() int {
// Outpoint Hash 32 bytes + Outpoint Index 4 bytes + Sequence 4 bytes +
// serialized varint size for the length of SignatureScript +
// SignatureScript bytes.
return 40 + VarIntSerializeSize(uint64(len(t.SignatureScript))) +
len(t.SignatureScript)
}
```
txout的字节占用:8字节转账金额、pkScript长度变长整数、pkScript占用字节数。
```
func (t *TxOut) SerializeSize() int {
// Value 8 bytes + serialized varint size for the length of PkScript +
// PkScript bytes.
return 8 + VarIntSerializeSize(uint64(len(t.PkScript))) + len(t.PkScript)
}
```
btcd在计算时加入了很多变长整数的字节占用,但是在其交易结构里面并没有对txin、txout的长度统计的字段,在使用该长度信息时在进行统计。比如在交易的序列化的过程中,对相应数据的长度进行了统计和写入。
```
func (msg *MsgTx) BtcEncode(w io.Writer, pver uint32, enc MessageEncoding) error {
...
// 计算txin的数量,并写入到io.Writer
count := uint64(len(msg.TxIn))
err = WriteVarInt(w, pver, count)
if err != nil {
return err
}
for _, ti := range msg.TxIn {
err = writeTxIn(w, pver, msg.Version, ti)
if err != nil {
return err
}
}
// 计算txout的数量,并写入io.Writer
count = uint64(len(msg.TxOut))
err = WriteVarInt(w, pver, count)
if err != nil {
return err
}
for _, to := range msg.TxOut {
err = WriteTxOut(w, pver, msg.Version, to)
if err != nil {
return err
}
}
...
```
下面通过一张图来说明整个流程
总结:btcd的这种统计内存占用的方式,其实和golang的内存分配和布局已经没有什么关联了,只是对bitcoin protocol的原始实现。虽然并不能体现其真实的底层内存占用情况,但是在一定程度反映了内存占用的变化情况。另外,在统计[]byte是并不是使用unsafe.SizeOf()函数简单实现,而是通过统计[]byte的长度,更贴近于真实情况,虽然golang真实占用比这个值要大。