我来自Python / Ruby / JavaScript背景。我了解指针的工作原理,但是,我不确定在以下情况下如何利用它们。
假设我们有一个虚构的Web API,该API搜索一些图像数据库并返回一个JSON,该JSON描述在找到的每个图像中显示的内容:
[
{
"url": "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"description": "Ocean islands",
"tags": [
{"name":"ocean", "rank":1},
{"name":"water", "rank":2},
{"name":"blue", "rank":3},
{"name":"forest", "rank":4}
]
},
...
{
"url": "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg",
"description": "Bridge over river",
"tags": [
{"name":"bridge", "rank":1},
{"name":"river", "rank":2},
{"name":"water", "rank":3},
{"name":"forest", "rank":4}
]
}
]
我的目标是在Go中创建一个数据结构,该结构将每个标签映射到如下所示的图像URL列表:
{
"ocean": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
],
"water": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"blue": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
],
"forest":[
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"bridge": [
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"river":[
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
]
}
如您所见,每个图像URL可以同时属于多个标签。如果我有成千上万个图像和更多标签,那么如果按每个标签的值复制图像URL字符串,则此数据结构会变得非常大。这是我要利用指针的地方。
我可以用Go中的两种结构表示JSON API响应,func searchImages()
模仿了伪造的API:
package main
import "fmt"
type Image struct {
URL string
Description string
Tags []*Tag
}
type Tag struct {
Name string
Rank int
}
// this function mimics json.NewDecoder(resp.Body).Decode(&parsedJSON)
func searchImages() []*Image {
parsedJSON := []*Image{
&Image {
URL: "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
Description: "Ocean islands",
Tags: []*Tag{
&Tag{"ocean", 1},
&Tag{"water", 2},
&Tag{"blue", 3},
&Tag{"forest", 4},
},
},
&Image {
URL: "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg",
Description: "Bridge over river",
Tags: []*Tag{
&Tag{"bridge", 1},
&Tag{"river", 2},
&Tag{"water", 3},
&Tag{"forest", 4},
},
},
}
return parsedJSON
}
现在,导致内存数据结构非常大的次优映射函数看起来像这样:
func main() {
result := searchImages()
tagToUrlMap := make(map[string][]string)
for _, image := range result {
for _, tag := range image.Tags {
// fmt.Println(image.URL, tag.Name)
tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], image.URL)
}
}
fmt.Println(tagToUrlMap)
}
我可以修改它以使用指向Image
结构URL
字段的指针,而不是按值复制它:
// Version 1
tagToUrlMap := make(map[string][]*string)
for _, image := range result {
for _, tag := range image.Tags {
// fmt.Println(image.URL, tag.Name)
tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], &image.URL)
}
}
它起作用了,我的第一个问题是,以这种方式构建映射后,result
数据结构会怎样? Image
URL
字符串字段会以某种方式保留在内存中,而其余的result
是否会被垃圾回收?还是result
数据结构会保留在内存中直到程序结束,因为某些内容指向其成员?
执行此操作的另一种方法是将URL复制到中间变量,然后使用指向它的指针:
// Version 2
tagToUrlMap := make(map[string][]*string)
for _, image := range result {
imageUrl = image.URL
for _, tag := range image.Tags {
// fmt.Println(image.URL, tag.Name)
tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], &imageUrl)
}
}
这更好吗? result
数据结构会被正确地垃圾回收吗?
或者也许我应该改用Image
结构中的字符串指针?
type Image struct {
URL *string
Description string
Tags []*Tag
}
有更好的方法吗?我还要感谢Go上的任何资源,这些资源深入描述了指针的各种用法。谢谢!
https://play.golang.org/p/VcKWUYLIpH7
更新:我担心最佳的内存消耗,并且最多不会生成不需要的垃圾。我的目标是使用尽可能少的内存。
答案 0 :(得分:4)
会正确收集垃圾吗?
是的
您永远不必担心会收集仍在使用的东西,一旦不再使用,您就可以依靠所收集的一切。
因此,关于GC的问题永远不会是“将其正确收集吗?”但是“我会产生不必要的垃圾吗?”。现在,这个实际问题不再取决于数据结构,而是取决于所创建的neu对象的数量(在堆上)。因此,这是一个有关如何使用数据结构的问题,而关于结构本身的问题则少得多。使用基准测试并使用-benchmem运行go测试。
(高端性能可能还会考虑GC必须完成的工作:扫描指针可能会花费一些时间。现在就算了。)
另一个相关的问题是关于记忆消耗。复制字符串仅复制三个单词,而复制* string则复制一个单词。因此,使用* string在这里没有什么安全的地方。
因此,不幸的是,相关问题(产生的垃圾量和总内存消耗)没有明确的答案。不要过分思考问题,请使用适合您目的的度量,度量和重构。
答案 1 :(得分:3)
首先了解一些背景。 Go中的string
值由类似结构的小型数据结构reflect.StringHeader
表示:
type StringHeader struct {
Data uintptr
Len int
}
因此,基本上传递/复制string
值会传递/复制此较小的struct值,无论string
的长度如何,该值仅为2个字。在64位体系结构上,即使string
具有一千个字符,也只有16个字节。
因此,基本上string
值已经用作指针。引入另一个指针,例如*string
只会使用法复杂化,并且您实际上不会获得任何明显的内存。为了进行内存优化,请忘记使用*string
。
它起作用了,我的第一个问题是,以这种方式构建映射后,结果数据结构会怎样?图片URL字符串字段会以某种方式保留在内存中,其余结果将被垃圾回收吗?还是结果数据结构会保留在内存中直到程序结束,因为某些内容指向其成员?
如果您有一个指向结构值字段的指针值,则整个结构将保留在内存中,无法进行垃圾回收。请注意,尽管可以释放为该结构的其他字段保留的内存,但是当前的Go运行时和垃圾收集器不会这样做。因此,为了获得最佳的内存使用率,您应该忘记存储结构字段的地址(除非您还需要完整的结构值,但仍然需要特别注意存储字段地址和切片/数组元素地址)。
这样做的原因是因为用于结构值的内存被分配为连续的段,因此仅保留一个引用的字段会严重破坏可用/可用内存,并使最佳内存管理变得越来越困难。高效。对这些区域进行碎片整理还需要复制引用字段的内存区域,这将需要“实时更改”指针值(更改内存地址)。
因此,在使用指向string
值的指针时,可以节省一些内存,但增加的复杂性和附加的间接性使其不值得。
那该怎么办?
所以最干净的方法是继续使用string
值。
还有我们之前没有提到的另一项优化。
您可以通过解组JSON API响应来获得结果。这意味着,如果JSON响应中多次包含相同的URL或标记值,则将为它们创建不同的string
值。
这是什么意思?如果在JSON响应中两次有相同的URL,则在解组后,将有2个不同的string
值,其中包含2个不同的指针,这些指针指向2个不同的分配字节序列(否则,字符串内容将是相同的)。 encoding/json
程序包不进行 string
实习。
这里有一个小应用程序证明了这一点:
var s []string
err := json.Unmarshal([]byte(`["abc", "abc", "abc"]`), &s)
if err != nil {
panic(err)
}
for i := range s {
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
fmt.Println(hdr.Data)
}
以上内容的输出(在Go Playground上尝试):
273760312
273760315
273760320
我们看到3个不同的指针。它们可能是相同的,因为string
的值是不变的。
json
包不会检测到重复的string
值,因为检测会增加内存和计算开销,这显然是不必要的。但是在我们的案例中,我们追求最佳的内存使用率,因此“初始”额外的计算确实值得获得大的内存增益。
所以让我们自己进行字符串实习。该怎么做?
解组JSON结果之后,在构建tagToUrlMap
映射期间,让我们跟踪遇到的string
值,如果先前看到的后续string
值只是使用该早期值(其字符串描述符)。
这是一个非常简单的字符串内部实现:
var cache = map[string]string{}
func interned(s string) string {
if s2, ok := cache[s]; ok {
return s2
}
// New string, store it
cache[s] = s
return s
}
让我们在上面的示例代码中测试此“合作伙伴”:
var s []string
err := json.Unmarshal([]byte(`["abc", "abc", "abc"]`), &s)
if err != nil {
panic(err)
}
for i := range s {
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
fmt.Println(hdr.Data, s[i])
}
for i := range s {
s[i] = interned(s[i])
}
for i := range s {
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
fmt.Println(hdr.Data, s[i])
}
以上内容的输出(在Go Playground上尝试):
273760312 abc
273760315 abc
273760320 abc
273760312 abc
273760312 abc
273760312 abc
太棒了!我们可以看到,在使用interned()
函数之后,"abc"
字符串中仅使用了一个实例(实际上是第一次出现)。这意味着所有其他实例(假设没有其他人使用它们)可以(并且将来)将被正确地进行垃圾收集(由垃圾收集器,在将来的某个时间)。
这里要记住的一件事:字符串内部专家使用cache
字典来存储所有以前遇到的字符串值。因此,要放开这些字符串,您还应该“清除”该缓存映射,最简单的方法是为其分配一个nil
值。
事不宜迟,让我们看看我们的解决方案:
result := searchImages()
tagToUrlMap := make(map[string][]string)
for _, image := range result {
imageURL := interned(image.URL)
for _, tag := range image.Tags {
tagName := interned(tag.Name)
tagToUrlMap[tagName] = append(tagToUrlMap[tagName], imageURL)
}
}
// Clear the interner cache:
cache = nil
要验证结果:
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(tagToUrlMap); err != nil {
panic(err)
}
输出为(在Go Playground上尝试):
{
"blue": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
],
"bridge": [
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"forest": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"ocean": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
],
"river": [
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
],
"water": [
"https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
"https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
]
}
我们使用内置的append()
函数将新的图像URL添加到标签中。 append()
可能(并且通常确实)分配了比需要更大的部分(考虑未来的增长)。完成“构建”过程后,我们可以遍历tagToUrlMap
映射并将这些切片“修剪”到所需的最小数量。
这是可以完成的方法:
for tagName, urls := range tagToUrlMap {
if cap(urls) > len(urls) {
urls2 := make([]string, len(urls))
copy(urls2, urls)
tagToUrlMap[tagName] = urls2
}
}