【问题标题】:How am I using less memory with more variables?我如何使用更少的内存和更多的变量?
【发布时间】:2021-12-29 20:15:27
【问题描述】:

首先,我要承认这个问题可能有一个我可以用谷歌搜索的答案,但我纯数学背景让我没有合适的词汇来知道要搜索什么。

我正在用 Go 编写代码并在 LeetCode 上解决这个问题:https://leetcode.com/problems/generate-parentheses/

我能够自己开发解决方案;像往常一样,我开始研究其他人如何解决问题并尝试优化时间和空间。在某个时候,我明白了这一点。

func generateParenthesis(n int) []string {
    
    ans := []string{}
    
    var dfs func([]byte, int, int) 
    dfs = func(path []byte, o, c int) {
        
        if c == n {
            ans = append(ans, string(path))
        }
        
        if o < n {
            dfs(append(path, '('), o + 1, c)
        }
        
        if o > c {
            dfs(append(path, ')'), o, c + 1)
        }
    }
    
    dfs([]byte{}, 0, 0)
    return ans

}

LeetCode 说这个解决方案占用了 2.8MB,即 p-88。我一直在寻找其他运行 2.7MB 的解决方案,即 p-100,并最终通过这样做得到了我的解决方案。

func generateParenthesis(n int) []string {
    
    ans := []string{}
    
    var dfs func(*[]string, []byte, int, int, int) 
    dfs = func(ans *[]string, path []byte, o, c, n int) {
        
        if c == n {
            *ans = append(*ans, string(path))
        }
        
        if o < n {
            dfs(ans, append(path, '('), o + 1, c, n)
        }
        
        if o > c {
            dfs(ans, append(path, ')'), o, c + 1, n)
        }
    }
    
    dfs(&ans, []byte{}, 0, 0, n)
    return ans

}

这些差异很小,很容易错过;递归 dfs 在第二个版本中需要更多参数。根据我对内存分配的了解,这没有任何意义。我希望向递归函数添加更多变量意味着每次将调用放在堆栈上时都需要分配更多内存。

  • 第二个版本增加了一个 int 和一个额外的指针。
  • LeetCode 可能在 64 位硬件上运行,这意味着 ints 和指针都需要额外的 64 位内存。
  • 堆栈上一次对dfs 的最大调用次数为2*nn 可以高达 8 次。

这一切都让我得出结论,第二个版本应该使用 (64 + 64) * 2 * 8 = 2048 位更多内存(可以忽略不计,tbf),但不知何故使用更少。

第一个问题:怎么会这样?

第二个问题:这是否可能特定于 Go 和/或正在使用的编译器?

【问题讨论】:

    标签: variables memory


    【解决方案1】:

    第二个版本确实在堆栈中传递了更多参数。据推测,这不足以超过 goroutine 堆栈容量,因此没有什么不同。默认 goroutine 堆栈大小为 8K,并以大块的形式增长。

    另一方面,第一个版本在闭包中捕获了额外的变量;该变量必须是堆分配的。

    将 dfs 声明为独立函数将通过摆脱闭包进一步减少内存使用量。

    【讨论】:

    • 我想我跟着。你是说第一个版本创建的闭包必须放在堆上,这有效地将所有内容从堆栈复制到堆?这让我想到了另一个问题……如果闭包被复制到堆中,每次调用 dfs 都会发生这种情况吗?如果不是每次调用 dfs 都发生这种情况,这是否意味着第一个版本会随着堆栈深度的增加而更好地扩展?
    • 闭包本身是堆分配的。函数引用的外部作用域中的每个变量也必须进行堆分配。原因很简单:如果捕获的变量被分配在堆栈上,并且外部函数返回,它们将不复存在,破坏可能仍然存在的闭包。 eli.thegreenplace.net/2019/…
    • 回答你的第二个问题。与常规函数调用相比,调用闭包不会做额外的工作,除了接收一个额外的隐藏参数。当闭包被实例化(dfs assignment)时,繁重的工作发生一次。所以确实,如果通过捕获参数的数量减少并且递归很深,那么将会取得一些胜利。但老实说,我认为这些微优化不应该应用于 leetcode 问题,而是应该专注于算法改进。
    猜你喜欢
    • 2022-06-17
    • 1970-01-01
    • 2015-08-15
    • 1970-01-01
    • 2017-03-04
    • 1970-01-01
    • 1970-01-01
    • 2013-04-02
    • 1970-01-01
    相关资源
    最近更新 更多