Skip to content

No =destroy for elements of closure environments other than for latest devel --gc:destructors #12577

Closed
@GordonBGood

Description

@GordonBGood

Although the "simple closures work" commit makes =destroy called for the elements of a closure environment on destruction of the closure with the "--gc:destructors" compiler switch, they aren't called generally for other Memory Management GC's (didn't check --newruntime).

This can cause memory leaks for "regular" GC's when fields of the environment contain objects containing fields that are manually allocated and depend on =destroy called on that object for their deallocation.

For the Alternative #2 using a big seq as a field, these are currently collected by GC's properly as shown by external resource management tools even though the occupied memory measured internally increases since seq is treated as a ref; however, this doesn't work with "--gc:destructors" even with the "simple closures work" patch as the occupied memory also continuously increases with each loop (confirmed externally) in spite of the destructor being called.

This issue will apply even more when "--seqsv2" becomes an option for all GC's as the new version of seq (and string) will depend on destructors, so will not work if =destroy is not called properly.

Example

Compiling with "--gc:destructors" now partially works (not for seq) as expected after the "simple closures work" PR, but using "regular" GC (of any type including default "refc") causes the problem behavior as the =destroy is not called on the environment contained TestObj.

import sugar

type
  TestObj = object
    big: pointer # Alternative 1
#    big: seq[byte] # alternative 2
    cntnts: int
proc `=destroy`(x: var TestObj) =
  # to show when destroyed...
  echo "destroying TestObj containing:  ", x.cntnts
  if x.big != nil: x.big.deallocShared # destruction for Alternative 1
#  x.big.`=destroy` # destruction for Alternative 2 - not working for --gc:destructors!
  x.wasMoved # system doesn't do this zeroing for us!
proc `=`(dst: var TestObj; src: TestObj) =
  echo "copying TestObj containing from/to:  ", src.cntnts, " ", dst.cntnts
  dst.`=destroy`; dst.big = src.big; dst.cntnts = src.cntnts
proc `=sink`(dst: var TestObj; src: TestObj) =
  echo "moving TestObj containing from/to:  ", src.cntnts, " ", dst.cntnts
  dst.`=destroy`; dst.big = src.big; dst.cntnts = src.cntnts
  
proc makeInnerClosure(): () -> void =
  let c = TestObj(big: (1024*1024).allocShared0, cntnts: 42) # the TestObj to be captured
#  let c = TestObj(big: newSeq[byte](1024*1024), cntnts: 42) #; c.big[] = newSeq[byte](1024*1024) # the TestObj to be captured
  result = () => c.cntnts.echo # by this returned closure!
  echo "as created:  ", c.cntnts # doesn't force a copy as it's using DFA to eliminate it?

proc main() =
  var tst: () -> void
  tst.`=sink` makeInnerClosure()
  stdout.write "as obtained:  "; tst()

for _ in 1 .. 10:
  GC_fullCollect() # in case GC hasn't yet actually disposed...
  let strtmem = getOccupiedMem()
  main()
  GC_fullCollect() # in case GC hasn't yet actually disposed...
  echo "change in occupied memory:  ", (getOccupiedMem() - strtmem)

Current Output

Repeated for every loop:

moving TestObj containing from/to:  42 0
destroying TestObj containing:  0
as created:  42
as obtained:  42
change in occupied memory:  1081272

Note: the change in occupied memory changes to a stable value after the first loop, and even at that is bigger than the expected 1024*1024 = 1048576 bytes it is expected to leak without proper destruction. The memory leak is confirmed by external resource monitoring tools, which when the number of loops is increased to 1000 show a continuous increase in memory use until the test program ends, when the memory use drops back to its original value.

Expected Output

Repeated for every loop:

moving TestObj containing from/to:  42 0
destroying TestObj containing:  0
as created:  42
as obtained:  42
destroying TestObj containing:  42
change in occupied memory:  0

The change in occupied memory should be 0. This output is as obtained from the latest devel including the "simple closures work" PR with the "--gc:destructors" compiler switch.

Possible Solution

Since the "simple closures work" PR patches make this work for "--gc:destructors" (where it didn't before that PR), the patch should not be limited to only when that compiler switch is on but on for all GC (and --newruntime?) compiler switches.

Additional Information

Tested on the devel branch as of 1-11-2019 including the "simple closures work" PR:

$ nim -v
Nim Compiler Version 1.1.0 [Windows: amd64]
Compiled at 2019-11-02
Copyright (c) 2006-2019 by Andreas Rumpf

active boot switches: -d:release

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions