【问题标题】:How to efficiently flatten a map?如何有效地展平地图?
【发布时间】:2021-02-01 18:29:49
【问题描述】:

我的应用需要采用大量嵌套的 map[string]interface{} 实例并将它们转换为平面地图。

例如:

{
                "foo": {
                    "jim":"bean"
                },
                "fee": "bar",
                "n1": {
                    "alist": [
                        "a",
                        "b",
                        "c",
                        {
                            "d": "other",
                            "e": "another"
                        }
                    ]
                },
                "number": 1.4567,
                "bool":   true
}

之后:

json.Unmarshal([]byte(input), &out)
result = Flatten(m.(map[string]interface{}))

变成:

{
                "foo.jim":      "bean",
                "fee":          "bar",
                "n1.alist.0":   "a",
                "n1.alist.1":   "b",
                "n1.alist.2":   "c",
                "n1.alist.3.d": "other",
                "n1.alist.3.e": "another",
                "number":       1.4567,
                "bool":         true,
}

目前我正在使用以下代码:

func Flatten(m map[string]interface{}) map[string]interface{} {
    o := map[string]interface{}{}
    for k, v := range m {
        switch child := v.(type) {
        case map[string]interface{}:
            nm := Flatten(child)
            for nk, nv := range nm {
                o[k+"."+nk] = nv
            }
        case []interface{}:
            for i := 0; i < len(child); i++ {
                o[k+"."+strconv.Itoa(i)] = child[i]
            }
        default:
            o[k] = v
        }
    }
    return o
}

此代码的内存效率非常低,当展平 ~800 个地图时,使用了 ~8 GiB 内存。 我尝试过传递指针而不是副本,但这不会改变内存使用情况。我是否必须进行某种类型的手动内存管理? Go 应该垃圾收集任何未使用的内存,但可能是我没有以某种方式释放它。

【问题讨论】:

  • 您是否尝试过只使用一个结果映射并将其传递给Flatten 的每次调用以及指向该点的路径(即连接键)?这样您就不必在每次调用时创建新映射并在 Flatten 返回后复制条目。

标签: go memory-leaks garbage-collection pass-by-reference flatten


【解决方案1】:

正如 Andy Schweig 在 cmets 中所建议的那样,传递在您遍历数据时更新的地图应该会显着减少内存使用量(注意:尚未测试)。比如:

func Flatten2(prefix string, src map[string]interface{}, dest map[string]interface{}) {
    if len(prefix) > 0 {
        prefix += "."
    }
    for k, v := range src {
        switch child := v.(type) {
        case map[string]interface{}:
            Flatten2(prefix+k, child, dest)
        case []interface{}:
            for i := 0; i < len(child); i++ {
                dest[prefix+k+"."+strconv.Itoa(i)] = child[i]
            }
        default:
            dest[prefix+k] = v
        }
    }
}

the playground 试试这个。

我是否必须进行某种类型的手动内存管理?

Go 运行时旨在为您处理内存管理(请参阅garbage collection 上的常见问题解答文章)。虽然垃圾收集器非常好,但它只能释放未使用的内存,并且您的原始例程在执行过程中创建了很多映射,直到处理完该级别的映射才能释放。

您可以通过environmental variables 或使用debug package 设置一些标志,但很少需要这些标志(它们可能会产生意想不到的后果)。

有什么建议可以进一步优化这个功能吗?

如果不访问您的数据集以及了解您的要求和有关如何测量内存使用情况的信息,很难发表评论(这可能比看起来更复杂)。 Profiling 您的应用程序可能会给您一些改进方面的建议。一种可能的改进是使用预先分配的切片而不是 dest 的映射(但我不知道您对结果做了什么,所以这可能不合适)。

【讨论】:

  • 这个策略显着减少了内存使用,非常有帮助。它现在对同一数据集使用约 2 GiB。有什么建议可以进一步优化这个功能吗?
  • 我加了几个cmets;但是最好的方法将取决于您的数据集(以及您需要结果的格式)。
  • dest 的切片是不错的选择,但需要在其他地方进行一些重构。
  • 我会在进行重大更改之前进行测试(差异可能不够显着而不值得)。
  • 数据集是来自FSCrawler的文件属性,每个地图可以有不同的大小和深度。所需的输出格式有点松散,但需要保持易于迭代。
【解决方案2】:

我刚刚写了一些代码来做这件事,享受吧!

import (
    "container/list"
    "fmt"
    "strings"
)

type pair struct {
    key    []string
    isRoot bool
    value  interface{}
}

// FlattenMap flattens a given Map.
func FlattenMap(input map[string]interface{}) map[string]interface{} {
    var returnValue = make(map[string]interface{})

    var stack = list.New()
    stack.PushBack(pair{key: []string{}, isRoot: true, value: input})

    for {
        element := stack.Back()
        if element == nil {
            break
        }
        stack.Remove(element)

        thePair, ok := element.Value.(pair)
        if !ok {
            panic("can't cast element to pair")
        }

        switch v := thePair.value.(type) {
        // Handle generic maps.
        case map[string]interface{}:
            for key, val := range v {
                if thePair.isRoot {
                    stack.PushBack(pair{key: []string{key}, value: val})
                } else {
                    newKeys := append(thePair.key, key)
                    stack.PushBack(pair{key: newKeys, value: val})
                }
            }
        // Handle arrays.
        case []interface{}:
            lastKey := thePair.key[len(thePair.key)-1]
            for index, val := range v {
                newKeys := make([]string, len(thePair.key))
                itemsCopied := copy(newKeys, thePair.key)
                newKeys[itemsCopied-1] = fmt.Sprintf("%s[%d]", lastKey, index)
                stack.PushBack(pair{key: newKeys, value: val})
            }
        // Handle simple values.
        default:
            returnValue[strings.Join(thePair.key, ".")] = v
        }
    }

    return returnValue
}

【讨论】:

  • 为什么返回指向地图的指针而不是地图?我认为绕过地图已经通过引用。
  • 我不知道,谢谢指出。我已经更新了代码。
  • 别担心,只是想知道这是否是一种更好的传递东西的方式:)
猜你喜欢
  • 1970-01-01
  • 2018-12-01
  • 1970-01-01
  • 2020-07-06
  • 1970-01-01
  • 1970-01-01
  • 2021-06-01
  • 2015-04-18
  • 1970-01-01
相关资源
最近更新 更多