function doit2(n::Int) s = BigInt(2234567876543456789876545678987654567898765456789876545678) for i = 1:n s += i end return s end
doit(10000000000)
On Mar 15, 2016 11:56 AM, "'Bill Hart' via julia-users" <julia...@googlegroups.com> wrote:
>
> We have been trying to understand the garbage collector behaviour, since we had some code for which our machine is running out of memory in a matter of an hour.
>
> We already realised that Julia isn't responsible for memory we allocate on the C side unless we use jl_gc_counted_malloc, which we now do everywhere. But it still uses masses of memory where we were roughly expecting no growth in memory usage (lots of short-lived objects and nothing much else).
Haven't finish the whole email but just want to point out first that julia is never responsible for freeing memory you malloced, (even with jl gc counted malloc) the function merely make the gc aware of the memory usage and you always need to free it explicitly.
On Mar 15, 2016 11:56 AM, "'Bill Hart' via julia-users" <julia...@googlegroups.com> wrote:
>
> We have been trying to understand the garbage collector behaviour, since we had some code for which our machine is running out of memory in a matter of an hour.
>
> We already realised that Julia isn't responsible for memory we allocate on the C side unless we use jl_gc_counted_malloc, which we now do everywhere. But it still uses masses of memory where we were roughly expecting no growth in memory usage (lots of short-lived objects and nothing much else).Haven't finish the whole email but just want to point out first that julia is never responsible for freeing memory you malloced, (even with jl gc counted malloc) the function merely make the gc aware of the memory usage and you always need to free it explicitly.
import Base: +
typealias ZZ Array{UInt, 1}
function +(a::ZZ, b::Int) r = ZZ(length(a)) ccall((:__gmpn_add_1, :libgmp), Void, (Ptr{UInt}, Ptr{UInt}, Int, Int), r, a, 3, b) return rend
function doit(n::Int) a = ZZ(3) a[1] = rand(UInt) a[2] = rand(UInt) a[3] = rand(UInt)
for s = 1:n a += s end
return aend
On Mar 15, 2016 11:56 AM, "'Bill Hart' via julia-users" <julia...@googlegroups.com> wrote:
>
> We have been trying to understand the garbage collector behaviour, since we had some code for which our machine is running out of memory in a matter of an hour.
>
> We already realised that Julia isn't responsible for memory we allocate on the C side unless we use jl_gc_counted_malloc, which we now do everywhere. But it still uses masses of memory where we were roughly expecting no growth in memory usage (lots of short-lived objects and nothing much else).
>
> The behaviour of the gc on my machine seems to be to allocate objects until 23mb of memory is allocated, then do a jl_gc_collect. However, even after reading as much of the GC code in C as I can, I still can't determine why we are observing the behaviour we are seeing.
>
> Here is a concrete example whose behaviour I don't understand:
>
> function doit2(n::Int)
> s = BigInt(2234567876543456789876545678987654567898765456789876545678)
> for i = 1:n
> s += i
> end
> return s
> end
>
> doit(10000000000)
>
>
> This is using Julia's BigInt type which is using a GMP bignum. Julia replaces the GMP memory manager functions with jl_gc_counted_malloc, so indeed Julia knows about all the allocations made here.
>
> But what I don't understand is that the memory usage of Julia starts at about 124mb and rises up to around 1.5gb. The growth is initially fast and it gets slower and slower.
I can't really reproduce this behavior.
I assume doit is doit2 and not another function you defined somewhere else.
>
> Can someone explain why there is this behaviour? Shouldn't jl_gc_collect be able to collect every one of those allocations every time it reaches the collect_interval of 23mb (which remains constant on my machine with this example)?
>
> As an alternative experiment, I implemented a kind of bignum type using Julia arrays of UInts which I then pass to GMP low level mpn functions (which don't do any allocations on the C side). I only implemented the + operator, just enough to make this example work.
>
> The behaviour in this case is that memory usage is constant at around 124mb. There is no growth in memory usage over time.
>
> Why is the one example using so much memory and the other is not?
>
> Note that the bignums do not grow here. They are always essentially 3 or 4 limbs or something like that, in both examples.
>
> Some other quick questions someone might be able to answer:
>
> * Is there any difference in GC behaviour between using & vs Ref in ccall?
Ref is heap allocated. & is not.
>
> * Does the Julia GC have a copying allocator for the short lived generation(s)?
It has two generation and not copy
>
> * Does the Julia GC do a full mark and sweep every collection?
No
> Even of the long lived generation(s)? If not, which part of the GC code is responsible for deciding when to do a more involved sweep vs a faster sweep. I am having some trouble orienting myself with the code, and I'd really like to understand it a bit better.
The counters in _jl_gc_collect
>
> * Can someone confirm whether the "pools" mentioned in the GC code refer to pools for different sized allocations. Are there multiple pools for the same sized allocation, or did I misunderstand that?
Pools are for small objects and they are segregated by size.
>
> Thanks in advance.
>
> Bill.
>
>
>
On Mar 15, 2016 11:56 AM, "'Bill Hart' via julia-users" <julia...@googlegroups.com> wrote:
>
> We have been trying to understand the garbage collector behaviour, since we had some code for which our machine is running out of memory in a matter of an hour.
>
> We already realised that Julia isn't responsible for memory we allocate on the C side unless we use jl_gc_counted_malloc, which we now do everywhere. But it still uses masses of memory where we were roughly expecting no growth in memory usage (lots of short-lived objects and nothing much else).
>
> The behaviour of the gc on my machine seems to be to allocate objects until 23mb of memory is allocated, then do a jl_gc_collect. However, even after reading as much of the GC code in C as I can, I still can't determine why we are observing the behaviour we are seeing.
>
> Here is a concrete example whose behaviour I don't understand:
>
> function doit2(n::Int)
> s = BigInt(2234567876543456789876545678987654567898765456789876545678)
> for i = 1:n
> s += i
> end
> return s
> end
>
> doit(10000000000)
>
>
> This is using Julia's BigInt type which is using a GMP bignum. Julia replaces the GMP memory manager functions with jl_gc_counted_malloc, so indeed Julia knows about all the allocations made here.
>
> But what I don't understand is that the memory usage of Julia starts at about 124mb and rises up to around 1.5gb. The growth is initially fast and it gets slower and slower.I can't really reproduce this behavior.
I assume doit is doit2 and not another function you defined somewhere else.
>
> Can someone explain why there is this behaviour? Shouldn't jl_gc_collect be able to collect every one of those allocations every time it reaches the collect_interval of 23mb (which remains constant on my machine with this example)?
>
> As an alternative experiment, I implemented a kind of bignum type using Julia arrays of UInts which I then pass to GMP low level mpn functions (which don't do any allocations on the C side). I only implemented the + operator, just enough to make this example work.
>
> The behaviour in this case is that memory usage is constant at around 124mb. There is no growth in memory usage over time.
>
> Why is the one example using so much memory and the other is not?
>
> Note that the bignums do not grow here. They are always essentially 3 or 4 limbs or something like that, in both examples.
>
> Some other quick questions someone might be able to answer:
>
> * Is there any difference in GC behaviour between using & vs Ref in ccall?Ref is heap allocated. & is not.
>
> * Does the Julia GC have a copying allocator for the short lived generation(s)?It has two generation and not copy
>
> * Does the Julia GC do a full mark and sweep every collection?No
> Even of the long lived generation(s)? If not, which part of the GC code is responsible for deciding when to do a more involved sweep vs a faster sweep. I am having some trouble orienting myself with the code, and I'd really like to understand it a bit better.
The counters in _jl_gc_collect
>
> * Can someone confirm whether the "pools" mentioned in the GC code refer to pools for different sized allocations. Are there multiple pools for the same sized allocation, or did I misunderstand that?Pools are for small objects and they are segregated by size.
>
> Thanks in advance.
>
> Bill.
>
>
>
>>
>> >
>> > * Does the Julia GC have a copying allocator for the short lived
>> > generation(s)?
>>
>> It has two generation and not copy
>
> So do the pools constitute the new generation? Or is the new generation
> separate from the pools?
They are all in the same pool
>
> I noticed in the code that it looks like things get promoted if they survive
> more than 2 generations (currently). But what happens to the objects between
more than 2 collection*
> generations 1 and 2 if they are not copied?
See the lifetime diagram in gc.c, they are marked differently.
>
> Or do you mean it has 2 generations plus a long term generation?
See also https://github.com/yuyichao/explore/blob/master/julia/new_gc/bit_swap.md
if you are interested.
>>
>> >
>> > * Does the Julia GC do a full mark and sweep every collection?
>>
>> No
>>
>> > Even of the long lived generation(s)? If not, which part of the GC code
>> > is responsible for deciding when to do a more involved sweep vs a faster
>> > sweep. I am having some trouble orienting myself with the code, and I'd
>> > really like to understand it a bit better.
>>
>> The counters in _jl_gc_collect
>
> You mean quick_count?
>
> In my version of Julia in gc.c, quick_count is set to 0 and incremented, but
> never used anywhere. How does this affect the behaviour? Is it used
> somewhere else in Julia?
>
> Or do you mean inc_count? I see this determines scanned_bytes_goal, which
> again is not used anywhere that I can see.
>
> I can't see anywhere else that inc_count is used either.
There are a lot of heuristics. see the logic that makes `sweep_mask =
GC_MARKED`. I believe I've recently removed those unused counters on
master.
>
> I noticed in the code that it looks like things get promoted if they survive
> more than 2 generations (currently). But what happens to the objects between
more than 2 collection*I see, so collections can happen at any time, not when some block of memory is "full".
> I see. That is a relief. We'll stick with & for now.
>
>> more than 2 collection*
>
>
> I see, so collections can happen at any time, not when some block of memory
> is "full".
Yes, it happens whenever the GC thinks there are enough allocation
since the last time.
> I wonder what the issue was, and whether the person who fixed it even realised they had fixed it. :-)
There were many GC related PRs. Some of them are for mem-leak. I don't
remember any one in particular about malloc though....
> Actually, let me ask this a bit more precisely.
>
> Suppose a collection has occurred. Then there are big gaps where memory has been reclaimed, between blocks that are still in use. If there is no copying collector, how does the gc avoid fragmentation?
>
> I see that each page of memory is released as it is found to contain no objects still in use. Is that the only mechanism used to control fragmentation?
It doesn't avoid fragmentation. This isn't a big issue for julia's
allocator since the pools are size segregated and it is never
necessary to find a slot of large enough size by walking the free
list.
This will cause more memory usage and I don't think there's a way
around it without moving. We'd like to try implement moving but as you
may guess this is a relatively hard problem. Fortunately I think
there's a relatively clear upgrade path although there are a lot of
other improvement we'd like to implement before that.
> So for you a "generation" has no relation to a "collection"?
I use collection only as in the "collection" in "doing a garbage collection"
>
> If not, what is your definition of a generation?
Julia GC has two generations. Objects are allocated in the young gen
and are promoted to the old gen if they survive long enough.
This is not much different from a traditional generational collector
although instead of using the memory space (address range) to keep
track of the metadata (the generation an object is in) we use the GC
bit to keep track of that info inline and avoid the moving.
>
> Does Julia just have two generations, short term and long term?
Yes, young and old.
> And each object in the short term generation has a collection count. Once the collection count hits 2 the object is promoted to long term generation by marking it, but it stays right where it is in the same "pool"?
Right.
The generational part of the GC is a little messy. This is mainly
because the new GC was designed to be an incremental GC and then
gradually transformed into a generational GC....... (they are
suprisingly similar....).
>> The generational part of the GC is a little messy. This is mainly
>> because the new GC was designed to be an incremental GC and then
>> gradually transformed into a generational GC....... (they are
>> suprisingly similar....).
>>
>
> Oh I didn't even realise that LOL. I thought Julia just switched to a
> generational gc and that's it. Probably at the time I didn't even know there
On master, yes, that's all, the incremental GC never hit the master.
I'm just talking about the PR that did this. In fact the original PR
still has the "wrong" title
https://github.com/JuliaLang/julia/pull/5227.
On Mar 15, 2016 4:16 PM, "'Bill Hart' via julia-users" <julia...@googlegroups.com> wrote:
>>
>>
>> >> The generational part of the GC is a little messy. This is mainly
>> >> because the new GC was designed to be an incremental GC and then
>> >> gradually transformed into a generational GC....... (they are
>> >> suprisingly similar....).
>> >>
>> >
>> > Oh I didn't even realise that LOL. I thought Julia just switched to a
>> > generational gc and that's it. Probably at the time I didn't even know there
>>
>> On master, yes, that's all, the incremental GC never hit the master.
>> I'm just talking about the PR that did this. In fact the original PR
>> still has the "wrong" title
>> https://github.com/JuliaLang/julia/pull/5227.
>
>
> So that PR was interesting reading. But now I have a new question (or two).
>
> Is it correct that the GC is not incremental, in the sense that low pause times were not gained by making the gc incremental (in the end), but by collecting the new generations more often than the old?
Right.
>
> Or is there now also some incremental behaviour as well?
No.
>
> Are there plans in this direction in the future?
Maybe. Not really on my own todo list and unlikely to be high priority.
>
> I was also unaware that LLVM makes it difficult to implement a moving GC. I hadn't realised that.
I didn't actually read the whole thread (it was also before I joint.) I assume you mean that using llvm instead of our own codegen backend makes moving harder. This is generally true for keeping track of any metadata in codegen when you don't control literally everything about it.