标签 Slice 下的文章

Go究竟是否为空切片分配了底层数组

本文永久链接 – https://tonybai.com/2022/02/15/whether-go-allocate-underlying-array-for-empty-slice

这周在“Go语言第一课”的留言区看到一位同学的这样一个问题:

切片是Go语言中的一个重要的语法元素,也是日常Go开发中使用最为频繁的语法元素。有过Go语言开发经验的童鞋估计大多都知道空切片(empty slice)与nil切片(nil slice)比较的梗,这也是Go面试中的一道高频题。

var sl1 = []int{} // sl1是空切片
var sl2 []int     // sl2是nil切片

要真正理解切片,离不开运行时的切片表示。在我的专栏《Go语言精进之路》一书中都有对切片在运行时表示的细致讲解。

切片在运行时由三个字段构成,reflect包中有切片在类型系统中表示的对应的定义:

// $GOROOT/src/reflect/value.go
type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

基于这个定义我们来理解空切片和nil切片就容易多了。我们用一段代码来看看这两种切片的差别:

// dumpslice.go
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    var sl1 = []int{}
    ph1 := (*reflect.SliceHeader)(unsafe.Pointer(&sl1))
    fmt.Printf("empty slice's header is %#v\n", *ph1)
    var sl2 []int
    ph2 := (*reflect.SliceHeader)(unsafe.Pointer(&sl2))
    fmt.Printf("nil slice's header is %#v\n", *ph2)
}

在这段代码中,我们通过unsafe包以及reflect.SliceHeader输出了空切片与nil切片在内存中的表示,即SliceHeader各个字段的值。我们在Go 1.18beta2下运行一下上述代码(使用-gcflags ‘-l -N’可关闭Go编译器的优化):

$go run -gcflags '-l -N' dumpslice.go
empty slice's header is reflect.SliceHeader{Data:0xc000092eb0, Len:0, Cap:0}
nil slice's header is reflect.SliceHeader{Data:0x0, Len:0, Cap:0}

通过输出结果,我们看到nil切片在运行时表示的三个字段值都是0;而空切片的len、cap值为0,但data值不为零

好了,此时我们再回到本文开始处那个童鞋提出的那个问题:空切片到底分没分配底层数组

答案是肯定的:没有分配!那么上述代码中空切片在运行时表示中第一个字段data的值0xc000092eb0从何而来,难道不是底层数组的地址么?

要想回答这个问题,我们需要下沉到汇编层面去看。

Go使用plan9的汇编语法,目前市面上关于这种汇编的资料比较少,比较权威是Go官方的asm资料和Rob Pike编写的A Manual for the Plan 9 assembler。此外IBM工程师的 Dropping down Go functions in assembly language这份资料也十分不错。国内《Go语言高级编程》一书以及曹春辉的plan9 assembly 完全解析讲解的十分全面,值得大家参考。

我们以下面这段最简单的有关空切片的代码为例:

// layout6.go

1 package main
2
3 func main() {
4     var sl = []int{}
5     _ = sl
6 }

生成go源码对应汇编代码的主要方法有:go tool compile -S xxx.go和针对编译后的二进制文件使用go tool objdump -S exe_file。

我们看看这段代码对应的汇编代码,我们使用下面命令将上述go源码转换为汇编代码(Go 1.18beta2 on darwin amd64):

$go tool compile -S -N -l layout6.go > layout6.s // -N -l两个命令行选项用于关闭Go编译器的优化,优化后的代码会掩盖实现细节

(在MacOS上)生成的layout6.s汇编代码如下(汇编代码中的FUNCDATA和PCDATA是Go编译器插入的、给GC使用的指示符,这里将其滤掉了):

"".main STEXT nosplit size=48 args=0x0 locals=0x30 funcid=0x0 align=0x0
    0x0000 00000 (layout6.go:3) TEXT    "".main(SB), NOSPLIT|ABIInternal, $48-0 // 48是main函数的栈帧大小,0表示参数大小
    0x0000 00000 (layout6.go:3) SUBQ    $48, SP
    0x0004 00004 (layout6.go:3) MOVQ    BP, 40(SP)
    0x0009 00009 (layout6.go:3) LEAQ    40(SP), BP
    0x000e 00014 (layout6.go:4) LEAQ    ""..autotmp_2(SP), AX
    0x0012 00018 (layout6.go:4) MOVQ    AX, ""..autotmp_1+8(SP)
    0x0017 00023 (layout6.go:4) TESTB   AL, (AX)
    0x0019 00025 (layout6.go:4) JMP 27
    0x001b 00027 (layout6.go:4) MOVQ    AX, "".sl+16(SP)
    0x0020 00032 (layout6.go:4) MOVUPS  X15, "".sl+24(SP)
    0x0026 00038 (layout6.go:6) MOVQ    40(SP), BP
    0x002b 00043 (layout6.go:6) ADDQ    $48, SP
    0x002f 00047 (layout6.go:6) RET
    0x0000 48 83 ec 30 48 89 6c 24 28 48 8d 6c 24 28 48 8d  H..0H.l$(H.l$(H.
    0x0010 04 24 48 89 44 24 08 84 00 eb 00 48 89 44 24 10  .$H.D$.....H.D$.
    0x0020 44 0f 11 7c 24 18 48 8b 6c 24 28 48 83 c4 30 c3  D..|$.H.l$(H..0.
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
    0x0000 6d 61 69 6e                                      main
""..inittask SNOPTRDATA size=24
    0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    0x0010 00 00 00 00 00 00 00 00                          ........
gclocals·33cdeccccebe80329f1fdbee7f5874cb SRODATA dupok size=8
    0x0000 01 00 00 00 00 00 00 00                          ........
gclocals·ff19ed39bdde8a01a800918ac3ef0ec7 SRODATA dupok size=9
    0x0000 01 00 00 00 04 00 00 00 00                       .........

关于汇编语法的问题,大家可以参考前面提供的参考资料,这里不赘述。我们这里最关注的是对应Go源码第4行Go代码的汇编源码,这里我把这段汇编源码单独提出来放在下面:

    0x000e 00014 (layout6.go:4) LEAQ    ""..autotmp_2(SP), AX
    0x0012 00018 (layout6.go:4) MOVQ    AX, ""..autotmp_1+8(SP)
    0x0017 00023 (layout6.go:4) TESTB   AL, (AX)
    0x0019 00025 (layout6.go:4) JMP 27
    0x001b 00027 (layout6.go:4) MOVQ    AX, "".sl+16(SP)
    0x0020 00032 (layout6.go:4) MOVUPS  X15, "".sl+24(SP)

我们逐行看一下:

  • 00014行:将SP寄存器指向的内存单元(该内存单元被命名为autotmp_2)的地址存入AX寄存器中;
  • 00019行:将AX寄存器中存储的值写入地址为SP+8的内存单元中,这个内存单元被命名为autotmp_1;
  • 00023行:将AL寄存器中的值与AX寄存器指向的内存单元的值做逻辑与操作,设置相关标志位;
  • 00025行:无条件跳转至00027行执行;
  • 00027行:将AX寄存器中存储的值写入sl切片变量运行时表示的第一个字段data中,该字段的地址为SP+16;
  • 00032行:使用intel平台上的SIMD指令集SSE的MOVUPS指令通过X15代表的固定的零寄存器对起始地址为SP+24的连续128bit(16个字节)进行清零。即sl切片变量运行时的len和cap字段被清零。

关于X15寄存器的含义,在Go internal ABI specification中有说明。

我这里用一幅图展示一下上面操作后的main函数栈情况:

我们看到切片sl的指向底层数组的指针data的值实际上是一个栈上的内存单元的地址,Go编译器并没有在堆上额外分配新的内存空间作为切片sl的底层数组。只是上面汇编代码的第00019行、00023行的操作让人很迷,不知道这两部指令操作的意图为何。

我们再来看一个例子,以进一步证实我们上面的结论。这个例子的源码如下:

// layout7.go
1 package main
2
3 func main() {
4     var sl = []int{}
5     sl = append(sl, 1)
6 }

在这个例子中,我们先是声明了一个空切片sl,之后又通过append为sl追加了一个元素。append时,由于sl为空切片,Go势必会为sl新分配底层存储数组,我们通过对比一下第4行和第5行两个操作的异同来确认“空切片并未分配底层数组”的结论。我们同样通过go tool compile -S命令得到该源码对应的汇编代码:

$go tool compile -S -N -l layout7.go > layout7.s

layout7.s中main函数的汇编代码如下(过滤掉了PCDATA和FUNCDATA指示符行):

"".main STEXT size=114 args=0x0 locals=0x70 funcid=0x0 align=0x0
    0x0000 00000 (layout7.go:3) TEXT    "".main(SB), ABIInternal, $112-0
    0x0000 00000 (layout7.go:3) CMPQ    SP, 16(R14)
    0x0004 00004 (layout7.go:3) JLS 107
    0x0006 00006 (layout7.go:3) SUBQ    $112, SP
    0x000a 00010 (layout7.go:3) MOVQ    BP, 104(SP)
    0x000f 00015 (layout7.go:3) LEAQ    104(SP), BP
    0x0014 00020 (layout7.go:4) LEAQ    ""..autotmp_2+64(SP), BX
    0x0019 00025 (layout7.go:4) MOVQ    BX, ""..autotmp_1+72(SP)
    0x001e 00030 (layout7.go:4) TESTB   AL, (BX)
    0x0020 00032 (layout7.go:4) JMP 34
    0x0022 00034 (layout7.go:4) MOVQ    BX, "".sl+80(SP)
    0x0027 00039 (layout7.go:4) MOVUPS  X15, "".sl+88(SP)
    0x002d 00045 (layout7.go:5) JMP 47
    0x002f 00047 (layout7.go:5) LEAQ    type.int(SB), AX
    0x0036 00054 (layout7.go:5) XORL    CX, CX
    0x0038 00056 (layout7.go:5) MOVQ    CX, DI
    0x003b 00059 (layout7.go:5) MOVL    $1, SI
    0x0040 00064 (layout7.go:5) CALL    runtime.growslice(SB)
    0x0045 00069 (layout7.go:5) LEAQ    1(BX), DX
    0x0049 00073 (layout7.go:5) JMP 75
    0x004b 00075 (layout7.go:5) MOVQ    $1, (AX)
    0x0052 00082 (layout7.go:5) MOVQ    AX, "".sl+80(SP)
    0x0057 00087 (layout7.go:5) MOVQ    DX, "".sl+88(SP)
    0x005c 00092 (layout7.go:5) MOVQ    CX, "".sl+96(SP)
    0x0061 00097 (layout7.go:6) MOVQ    104(SP), BP
    0x0066 00102 (layout7.go:6) ADDQ    $112, SP
    0x006a 00106 (layout7.go:6) RET
    0x006b 00107 (layout7.go:6) NOP
    0x006b 00107 (layout7.go:3) CALL    runtime.morestack_noctxt(SB)
    0x0070 00112 (layout7.go:3) JMP 0
    ... ...

有了对layout6.s的汇编的分析的基础,再来看这段汇编似乎就好很多了。首先layout7.s中对应var sl = []int{}代码的第00020到00039的原理与layout6.s一致。sl的data字段被赋值为一个栈上内存单元(SP+64)的地址。

从第00047到00073实际上是为调用runtime.growslice函数做准备以及调用runtime.growslice函数。runtime.growslice函数负责在堆上分配新的底层数组用于存储切片sl的元素。runtime.growslice返回后,我们看到,第00075行,Go将一个立即数1写入AX寄存器指向的内存单元,即growslice新分配的底层数组的第一个元素的内存单元。

之后,sl的三个字段被重新做了赋值:

    0x0052 00082 (layout7.go:5) MOVQ    AX, "".sl+80(SP)
    0x0057 00087 (layout7.go:5) MOVQ    DX, "".sl+88(SP)
    0x005c 00092 (layout7.go:5) MOVQ    CX, "".sl+96(SP)

我们看到:00082行,sl的data字段(SP+80)被赋值为AX寄存器中的值,即堆上分配新的底层数组的地址。而后的len和cap字段也分配用DX和CX寄存器的值做了赋值,这两个寄存器分配存储了切片的len和cap。

我这里同样用一幅示意图展示append后main函数栈的情况:

通过这个例子,我们可以看到,如果Go在堆上为切片分配底层数组,我们会在汇编代码中看到growslice或newobject这样的调用。

如果一个非空切片没有逃逸到堆上,那么Go也可能在栈上为该切片分配底层数组空间,比如下面这段代码:

// layout10.go
package main

func main() {
    var sl = []int{11, 12, 13}
    _ = sl
}

它对应的汇编如下:

"".main STEXT nosplit size=103 args=0x0 locals=0x40 funcid=0x0 align=0x0
    0x0000 00000 (layout10.go:3)    TEXT    "".main(SB), NOSPLIT|ABIInternal, $64-0
    0x0000 00000 (layout10.go:3)    SUBQ    $64, SP
    0x0004 00004 (layout10.go:3)    MOVQ    BP, 56(SP)
    0x0009 00009 (layout10.go:3)    LEAQ    56(SP), BP
    0x000e 00014 (layout10.go:4)    MOVUPS  X15, ""..autotmp_2(SP)
    0x0013 00019 (layout10.go:4)    MOVUPS  X15, ""..autotmp_2+8(SP)
    0x0019 00025 (layout10.go:4)    LEAQ    ""..autotmp_2(SP), AX
    0x001d 00029 (layout10.go:4)    MOVQ    AX, ""..autotmp_1+24(SP)
    0x0022 00034 (layout10.go:4)    TESTB   AL, (AX)
    0x0024 00036 (layout10.go:4)    MOVQ    $11, ""..autotmp_2(SP)
    0x002c 00044 (layout10.go:4)    TESTB   AL, (AX)
    0x002e 00046 (layout10.go:4)    MOVQ    $12, ""..autotmp_2+8(SP)
    0x0037 00055 (layout10.go:4)    TESTB   AL, (AX)
    0x0039 00057 (layout10.go:4)    MOVQ    $13, ""..autotmp_2+16(SP)
    0x0042 00066 (layout10.go:4)    TESTB   AL, (AX)
    0x0044 00068 (layout10.go:4)    JMP 70
    0x0046 00070 (layout10.go:4)    MOVQ    AX, "".sl+32(SP)
    0x004b 00075 (layout10.go:4)    MOVQ    $3, "".sl+40(SP)
    0x0054 00084 (layout10.go:4)    MOVQ    $3, "".sl+48(SP)
    0x005d 00093 (layout10.go:6)    MOVQ    56(SP), BP
    0x0062 00098 (layout10.go:6)    ADDQ    $64, SP
    0x0066 00102 (layout10.go:6)    RET

这段汇编代码就留给大家自己阅读分析吧。


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强,欢迎大家加入!

img{512x368}

img{512x368}
img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go 1.18对泛型的支持策略

img{512x368}

本文永久链接 – https://tonybai.com/2021/10/28/expectations-for-generics-in-go-1.18

2021年10月中旬,Go语言之父Rob Pike在github上的Go项目中发了一条issue:建议不在Go 1.18的标准库中使用泛型

不得不说“姜还是老的辣”!Rob Pike的理由很简单,Go泛型是Go诞生以来最大的一次语言变化,Go 1.18版本承载了太多的change,容易出错。并且Go核心开发团队也没有使用新泛型的经验,他建议Go核心开发团队应该多等待、观察和学习。我是十分赞同Rob Pike的建议的,不要把步子迈得太大。Go应该按照自己的节奏稳步前进。

Rob Pike的这个issue引发了Go核心团队与社区的热烈响应。离Go 1.18版本发布还有4个月左右的时间了,后续Go泛型到底如何落地,整个Go社区需要一个明确的方向。

今天,Go核心团队技术负责人Russ Cox在golang-dev group发文,针对Rob Pike的issue介绍了Go 1.18版本与泛型当前进展与后续的支持策略,这确定了Go核心团队与社区的努力方向。这里粗略翻译一下供大家参考。


如果没有意外的严重问题,Go 1.18版本将包含对泛型的支持。泛型是Go1发布以来最重要的变化,当然也是我们有史以来最大的一次语言变化。这封邮件粗略解释了泛型的加入对我们和用户的意义。

任何Go的新功能特性,无论是语言还是库,都带有不确定性,包括不确定如何使用它们,不确定如何不使用它们,以及不确定有哪些微小的bug已经通过了现有的测试集。泛型也不能避免这种不确定性;事实上,因为泛型是一个大型的新功能,所以它的不确定性也相应地更大。

因为我们不知道使用泛型的最佳实践是什么,所以我们的文档将无法就何时使用泛型和何时不使用泛型给出精确、明确的答案。即便我们仍然可以并将给出粗略的泛型使用指南。作为比较,我们是在不间断地写了一整年的Go代码后,才写出了Effective Go的最初版本的。我们在泛型方面同样还没有较高水平使用经验,所以我们当然会提供关于如何使用泛型的文档,但我们短期内不能提供任何关于泛型代码风格和最佳实践方面的指南性文档。很简单,因为我们也欠缺这方面的实践与经验。

因为我们不知道编写泛型包的最佳实践是什么,所以我们发布的最初的泛型代码–特别是通过提案程序的maps和slices包–将首先放在golang.org/x/exp中,那里不能保证向后兼容。一旦我们有了更多的经验,我们希望能将其中一些包推广到标准库中。唯一例外的是constraints包,它是编写某些泛型代码的基础,它将在Go 1.18中就被添加到标准库中。

因为我们没有任何关于泛型的生产经验,所以我们会在发布说明中明确指出,在生产中使用泛型的时候应该适当谨慎。这并不是对Go核心团队出色工作的批评。这只是一个观察,泛型与大多数Go的变化不同。当我们重写垃圾收集器改变调用惯例时,我们会在测试和生产中使用新的实现来运行谷歌的所有Go程序,这样就能很好地验证变化,揪出难以发现的错误。相比之下,用正在进行中的Go 1.18工具链重建非泛型代码并不能验证对泛型的支持,这意味着我们无法建立同样的信心。

综上所述,Go 1.18与其他Go 1.x版本一样具有向后兼容的承诺:我们不会破坏用Go 1.18构建的代码,包括使用泛型的代码。在最坏的情况下,如果我们发现Go 1.18的语义有一些致命的问题,并需要改变它们(例如在Go 1.19中),我们将使用go.mod文件的go版本指示符来确定该module中的源文件是使用Go 1.18还是Go 1.19+的语义。(我们预计不需要这样做!)

我们预想到一些包的作者可能会急于采用泛型。如果您正在更新您的软件包以使用泛型,请考虑将新的泛型API隔离到自己的文件中,并为其使用Go 1.18的构建标签(//go:build go1.18),以便Go 1.17用户可以继续构建和使用非泛型部分。

同样值得注意的是,第三方工具可能不会在Go 1.18发布时完全支持泛型。我们正在与许多工具的作者交谈,并试图确保他们得到适当的更新,但各个工具都有自己的时间表。

我们收到的一个常见的问题是:考虑到所有这些不确定性,为什么不把泛型变成可选项加入Go 1.18?答案是,在这一点上,减少不确定性的唯一方法是让其默认可用。当我们在Go 1.5版本中让vendor机制作为可选项加入时,发生的情况是几乎没有人真正使用它,直到Go 1.6版本默认开启它。所以Go 1.5版本没有减少我们对Go开发者使用vendor情况的不确定性。另一方面,Go 1.5版本无疑将生态系统分为”在标准Go下运行的代码”和 “在启用vendoring后运行的代码”两个部分。我们希望在这里尽可能地避免这种结果。

这里每个人可以做的最重要的事情就是写一些泛型代码,如果你发现了bug,不清楚的编译器错误等等,请让我们知道。我最近写了一些泛型数据结构,对整体的体验非常满意。我希望你也会这样;如果没有,请提交bug。谢谢!


“Gopher部落”知识星球正式转正(从试运营星球变成了正式星球)!“gopher部落”旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!部落目前虽小,但持续力很强,欢迎大家加入!

img{512x368}

img{512x368}
img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 微信公众号:iamtonybai
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • “Gopher部落”知识星球:https://public.zsxq.com/groups/51284458844544

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats