[インデックス 18790] ファイルの概要
このコミットは、Goランタイムにおけるゴルーチンのスタック縮小メカニズムを大幅に改善するものです。特に、スタックの内容をコピーすることなく、その場でスタックを縮小する新しいアプローチを導入しています。これにより、Cコード内でブロックしているゴルーチンが以前に確保した過剰なスタック領域を効率的に解放できるようになります。
コミット
commit f4359afa7f7886541a51c44cefee39250a202d65
Author: Keith Randall <khr@golang.org>
Date: Thu Mar 6 16:03:43 2014 -0800
runtime: shrink bigger stacks without any copying.
Instead, split the underlying storage in half and
free just half of it.
Shrinking without copying lets us reclaim storage used
by a previously profligate Go routine that has now blocked
inside some C code.
To shrink in place, we need all stacks to be a power of 2 in size.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews
https://golang.org/cl/69580044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f4359afa7f7886541a51c44cefee39250a202d65
元コミット内容
このコミットは、Goランタイムのスタック管理における重要な変更を導入しています。主な目的は、ゴルーチンのスタックが過剰に大きくなった場合に、そのスタックをコピーすることなく効率的に縮小できるようにすることです。
従来のスタック縮小は、新しい小さいスタックを割り当て、古いスタックの内容を新しいスタックにコピーし、古いスタックを解放するというプロセスで行われていました。しかし、この方法にはいくつかの課題がありました。特に、Cコード内でブロックしているゴルーチンの場合、Goランタイムがスタックをコピーしようとすると問題が発生する可能性がありました。
このコミットでは、スタックの基盤となるストレージを半分に分割し、その半分を解放するという新しいアプローチを採用しています。この「コピーなし」の縮小は、特にCコードとの相互運用において、以前に大量のスタックを使用したゴルーチンがブロックされた場合に、そのストレージを再利用できるという利点があります。このインプレース縮小を可能にするために、すべてのスタックサイズが2のべき乗である必要があるという制約が導入されています。
変更の背景
Goのゴルーチンは、非常に軽量なスレッドであり、必要に応じてスタックを動的に拡大・縮小します。スタックの拡大は比較的単純ですが、縮小はより複雑な問題です。
従来のGoランタイムでは、ゴルーチンのスタックが大きくなりすぎた場合、copystack
関数を使用してスタックを縮小していました。これは、より小さい新しいスタック領域を割り当て、古いスタックの内容を新しい領域にコピーし、その後古いスタック領域を解放するというプロセスでした。
このコピーベースの縮小にはいくつかの問題がありました。
- パフォーマンスオーバーヘッド: 大量のデータをコピーする必要があるため、パフォーマンスに影響を与える可能性がありました。
- Cgoとの相互作用: GoのゴルーチンがCコードを呼び出し、そのCコード内でブロックしている場合、Goランタイムがスタックをコピーしようとすると、CスタックフレームとGoスタックフレームの間の整合性が失われ、問題が発生する可能性がありました。特に、CコードがGoスタック上のポインタを保持している場合、スタックの移動は危険でした。
- メモリの再利用の遅延: Cコード内でブロックしているゴルーチンが、以前に大量のスタックを消費していた場合、そのスタック領域はすぐに解放されず、メモリの効率的な再利用が妨げられる可能性がありました。
このコミットの背景にあるのは、これらの課題を解決し、より効率的で安全なスタック縮小メカニズムを提供することです。特に、Cコードとの相互運用性を向上させ、メモリの再利用を促進することが重要な目標でした。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムの概念とメモリ管理の仕組みについて理解しておく必要があります。
-
ゴルーチンスタック:
- Goのゴルーチンは、OSのスレッドとは異なり、非常に小さい初期スタック(通常は数KB)で開始します。
- 関数呼び出しが深くなるにつれて、必要に応じてスタックは自動的に拡大します。
- 関数から戻るなどしてスタックの使用量が減ると、スタックは縮小される可能性があります。
- スタックは連続したメモリ領域として割り当てられます。
-
スタックガード (StackGuard):
- Goのスタックは、スタックオーバーフローを検出するために「スタックガード」と呼ばれるメカニズムを使用します。
- スタックの末尾近くにガードページが設定されており、スタックポインタがこのガードページに到達すると、ランタイムがスタックの拡大処理(または縮小処理)を開始します。
-
メモリヒープ (MHeap):
- Goランタイムは、独自のメモリマネージャを持っています。
MHeap
は、Goプログラムが使用するすべてのメモリを管理するグローバルなヒープです。 MHeap
は、メモリをMSpan
と呼ばれる大きなチャンクに分割して管理します。
- Goランタイムは、独自のメモリマネージャを持っています。
-
MSpan:
MSpan
は、Goランタイムのメモリ管理における基本的な単位です。これは、連続したページ(PageSize
の倍数)のブロックを表します。MSpan
は、オブジェクトの割り当てやスタックの割り当てに使用されます。MSpan
には、その状態(使用中、空きなど)、ページ数、サイズクラスなどの情報が含まれます。
-
ページサイズ (PageSize):
- Goランタイムは、OSのメモリページサイズ(通常は4KB)を基盤としてメモリを管理します。
PageSize
は、メモリ割り当ての最小単位です。
- Goランタイムは、OSのメモリページサイズ(通常は4KB)を基盤としてメモリを管理します。
-
FixedStack
:- Goのゴルーチンスタックには、
FixedStack
という最小サイズがあります。スタックはこれ以上小さくはなりません。
- Goのゴルーチンスタックには、
-
copystack
:- このコミット以前のGoランタイムでスタックを縮小する際に使用されていた関数です。
- 新しい小さいスタック領域を割り当て、古いスタックの内容を新しい領域にコピーし、古いスタックを解放するという動作をしていました。
-
2のべき乗のスタックサイズ:
- このコミットで導入される重要な制約です。インプレースでのスタック縮小を可能にするために、スタックのサイズが常に2のべき乗(例: 4KB, 8KB, 16KBなど)である必要があります。これにより、スタック領域を正確に半分に分割し、その半分を解放することが容易になります。
これらの概念を理解することで、このコミットがGoランタイムのスタック管理にどのような影響を与え、なぜこのような変更が必要とされたのかを深く把握することができます。
技術的詳細
このコミットの核心は、Goランタイムがゴルーチンのスタックを「コピーなし」で縮小する新しいメカニズムを導入した点にあります。これは、特にCコードとの相互運用性における課題を解決し、メモリの効率的な再利用を促進することを目的としています。
従来のスタック縮小の課題と新しいアプローチ
従来のスタック縮小は、copystack
関数によって行われていました。これは、新しいより小さいスタック領域を割り当て、古いスタックの内容を新しい領域にコピーし、その後古いスタックを解放するというものでした。このアプローチは、Goコード内でのスタック縮小には機能しましたが、以下のような問題がありました。
- Cgoとの相互作用: GoのゴルーチンがCコードを呼び出し、そのCコード内で長時間ブロックしている場合(例: ネットワークI/O、システムコールなど)、Goランタイムがスタックをコピーしようとすると問題が発生する可能性がありました。CコードはGoスタック上のポインタを保持している可能性があり、スタックが移動するとこれらのポインタが無効になり、クラッシュやデータ破損を引き起こす可能性がありました。
- メモリの非効率性: Cコード内でブロックしているゴルーチンが、以前に大量のスタックを消費していた場合、そのスタック領域はすぐに解放されず、メモリの効率的な再利用が妨げられていました。
このコミットでは、これらの問題を解決するために、スタックの基盤となるストレージをその場で半分に分割し、使用されていない下半分を解放するという新しいアプローチを採用しています。これにより、スタックの内容をコピーする必要がなくなり、Cコードとの相互作用の問題を回避しつつ、メモリを迅速に再利用できるようになります。
2のべき乗のスタックサイズ要件
インプレースでのスタック縮小を可能にするために、このコミットでは重要な制約が導入されています。それは、すべてのスタックサイズが2のべき乗である必要があるという点です。
runtime·stackalloc
の変更: スタックを割り当てる際に、要求されたサイズが2のべき乗でない場合にパニックを発生させるチェックが追加されました。round2
関数の導入: 任意の数値を、それ以上の最小の2のべき乗に丸めるround2
ヘルパー関数が追加されました。runtime·newstack
の変更: 新しいスタックを割り当てる際に、framesize
をround2
関数で丸めるように変更されました。これにより、常に2のべき乗のサイズのスタックが割り当てられるようになります。
この制約は、スタック領域を正確に半分に分割し、その半分を解放するという新しい縮小メカニズムの基盤となります。例えば、16KBのスタックであれば、8KBずつに分割し、下位の8KBを解放することができます。
runtime·MHeap_SplitSpan
関数の導入
このコミットのもう一つの重要な変更は、runtime·MHeap_SplitSpan
関数の導入です。この関数は、MSpan
(Goランタイムのメモリ管理単位)を2つの等しい部分に分割する役割を担います。
- 機能: 割り当て済みの
MSpan
を受け取り、それを2つの新しいMSpan
に分割します。元のMSpan
は後半部分を保持し、新しいMSpan
が前半部分を保持します。 - 用途: スタックのインプレース縮小において、スタックが複数のページにまたがる場合に、そのスタックを構成する
MSpan
を分割するために使用されます。これにより、スタックの下半分を構成するMSpan
を独立して解放できるようになります。 - 制約: 分割される
MSpan
のページ数が偶数であること、MSpanInUse
状態であること、および参照カウントが1であること(スタックが単一のオブジェクトとして割り当てられていることを意味する)が前提となります。
runtime·shrinkstack
の変更
runtime·shrinkstack
関数は、ゴルーチンのスタックを縮小する主要な関数であり、このコミットで最も大きな変更が加えられました。
- コピーベースの縮小の条件付き維持:
newsize
がPageSize/2
(通常2KB)未満の場合、またはsyscallstack
が設定されている場合(Cgo呼び出し中など)、依然としてcopystack
を使用してスタックをコピーする動作が残されています。これは、非常に小さいスタックへの縮小や、特定のCgoのケースではインプレース縮小が困難または不適切な場合があるためです。 - インプレース縮小のロジック:
- スタックの基盤となる
MSpan
をruntime·MHeap_LookupMaybe
で検索します。 - スタックがヒープ外に割り当てられている場合(まれなケース)、インプレース縮小は行われません。
- スタックが1ページサイズ(通常4KB)の場合、
MSpan
のref
、sizeclass
、elemsize
を直接変更することで、あたかも2つの半分のサイズのオブジェクトとして割り当てられたかのように「騙し」、下半分を解放できるようにします。 - スタックが1ページより大きい場合、
runtime·MHeap_SplitSpan
を呼び出してMSpan
を2つに分割し、下半分を構成するMSpan
を独立して解放できるようにします。 - 新しいスタックガード(
stackguard
とstackguard0
)とスタックベース(stack0
)が、縮小されたスタックの新しいサイズに合わせて更新されます。 - 最後に、
runtime·free(oldstk)
を呼び出すことで、スタックの下半分が解放されます。
- スタックの基盤となる
利点
この新しいインプレース縮小メカニズムは、以下の重要な利点をもたらします。
- Cgoとの互換性向上: スタックをコピーする必要がなくなるため、Cコード内でブロックしているゴルーチンのスタックを安全に縮小できるようになります。これにより、Cgoを使用するアプリケーションの安定性とパフォーマンスが向上します。
- メモリの効率的な再利用: 過剰に確保されたスタック領域が迅速に解放されるため、メモリの断片化が減少し、全体的なメモリ使用効率が向上します。
- パフォーマンスの向上: スタックコピーのオーバーヘッドがなくなるため、スタック縮小のパフォーマンスが向上します。
このコミットは、Goランタイムのメモリ管理、特にスタック管理の堅牢性と効率性を大幅に向上させる重要な一歩と言えます。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、以下の3つのファイルに集中しています。
-
src/pkg/runtime/malloc.h
:runtime·MHeap_SplitSpan
関数のプロトタイプ宣言が追加されました。
--- a/src/pkg/runtime/malloc.h +++ b/src/pkg/runtime/malloc.h @@ -525,6 +525,7 @@ void* runtime·MHeap_SysAlloc(MHeap *h, uintptr n); void runtime·MHeap_MapBits(MHeap *h); void runtime·MHeap_MapSpans(MHeap *h); void runtime·MHeap_Scavenger(void); +void runtime·MHeap_SplitSpan(MHeap *h, MSpan *s); void* runtime·mallocgc(uintptr size, uintptr typ, uint32 flag); void* runtime·persistentalloc(uintptr size, uintptr align, uint64 *stat);
-
src/pkg/runtime/mheap.c
:runtime·MHeap_SplitSpan
関数の実装が追加されました。この関数は、与えられたMSpan
を2つの等しい部分に分割し、ヒープの管理構造を更新します。
--- a/src/pkg/runtime/mheap.c +++ b/src/pkg/runtime/mheap.c @@ -840,3 +840,54 @@ runtime·freeallspecials(MSpan *span, void *p, uintptr size) runtime·throw("can't explicitly free an object with a finalizer"); } } + +// Split an allocated span into two equal parts. +void +runtime·MHeap_SplitSpan(MHeap *h, MSpan *s) +{ + MSpan *t; + uintptr i; + uintptr npages; + PageID p; + + if((s->npages & 1) != 0) + runtime·throw("MHeap_SplitSpan on an odd size span"); + if(s->state != MSpanInUse) + runtime·throw("MHeap_SplitSpan on a free span"); + if(s->sizeclass != 0 && s->ref != 1) + runtime·throw("MHeap_SplitSpan doesn't have an allocated object"); + npages = s->npages; + + runtime·lock(h); + + // compute position in h->spans + p = s->start; + p -= (uintptr)h->arena_start >> PageShift; + + // Allocate a new span for the first half. + t = runtime·FixAlloc_Alloc(&h->spanalloc); + runtime·MSpan_Init(t, s->start, npages/2); + t->limit = (byte*)((t->start + npages/2) << PageShift); + t->state = MSpanInUse; + t->elemsize = npages << (PageShift - 1); + t->sweepgen = s->sweepgen; + if(t->elemsize <= MaxSmallSize) { + t->sizeclass = runtime·SizeToClass(t->elemsize); + t->ref = 1; + } + + // the old span holds the second half. + s->start += npages/2; + s->npages = npages/2; + s->elemsize = npages << (PageShift - 1); + if(s->elemsize <= MaxSmallSize) { + s->sizeclass = runtime·SizeToClass(s->elemsize); + s->ref = 1; + } + + // update span lookup table + for(i = p; i < p + npages/2; i++) + h->spans[i] = t; + + runtime·unlock(h); +}
-
src/pkg/runtime/stack.c
:runtime·stackalloc
に、スタックサイズが2のべき乗であることのチェックが追加されました。round2
ヘルパー関数が追加されました。runtime·newstack
でframesize
をround2
で丸めるように変更されました。runtime·shrinkstack
関数が大幅に修正され、コピーなしのインプレース縮小ロジックが導入されました。
--- a/src/pkg/runtime/stack.c +++ b/src/pkg/runtime/stack.c @@ -94,6 +94,8 @@ runtime·stackalloc(uint32 n) // Doing so would cause a deadlock (issue 1547). if(g != m->g0) runtime·throw("stackalloc not on scheduler stack"); + if((n & (n-1)) != 0) + runtime·throw("stack size not a power of 2"); if(StackDebug >= 1) runtime·printf("stackalloc %d\n", n); @@ -536,6 +538,18 @@ copystack(G *gp, uintptr nframes, uintptr newsize) runtime·stackfree(oldstk, oldsize); } +// round x up to a power of 2. +static int32 +round2(int32 x) +{ + int32 s; + + s = 0; + while((1 << s) < x) + s++; + return 1 << s; +} + // Called from runtime·newstackcall or from runtime·morestack when a new // stack segment is needed. Allocate a new stack big enough for // m->moreframesize bytes, copy m->moreargsize bytes to the new frame, @@ -654,6 +668,7 @@ runtime·newstack(void) if(framesize < StackMin) framesize = StackMin; framesize += StackSystem; + framesize = round2(framesize); gp->stacksize += framesize; if(gp->stacksize > runtime·maxstacksize) { runtime·printf("runtime: goroutine stack exceeds %D-byte limit\n", (uint64)runtime·maxstacksize); @@ -744,26 +759,65 @@ runtime·shrinkstack(G *gp) { int32 nframes; byte *oldstk, *oldbase; - uintptr used, oldsize; - - if(gp->syscallstack != (uintptr)nil) // TODO: handle this case? - return; + uintptr used, oldsize, newsize; + MSpan *span; oldstk = (byte*)gp->stackguard - StackGuard; oldbase = (byte*)gp->stackbase + sizeof(Stktop); oldsize = oldbase - oldstk; - if(oldsize / 2 < FixedStack) + newsize = oldsize / 2; + if(newsize < FixedStack) return; // don't shrink below the minimum-sized stack used = oldbase - (byte*)gp->sched.sp; if(used >= oldsize / 4) return; // still using at least 1/4 of the segment. - nframes = copyabletopsegment(gp); - if(nframes == -1) - return; // TODO: handle this case. Shrink in place? - - copystack(gp, nframes, oldsize / 2); + // To shrink to less than 1/2 a page, we need to copy. + if(newsize < PageSize/2) { + if(gp->syscallstack != (uintptr)nil) // TODO: can we handle this case? + return; +#ifdef GOOS_windows + if(gp->m != nil && gp->m->libcallsp != 0) + return; +#endif + nframes = copyabletopsegment(gp); + if(nframes == -1) + return; + copystack(gp, nframes, newsize); + return; + } - if(StackDebug >= 1) - runtime·printf("stack shrink done\n"); + // To shrink a stack of one page size or more, we can shrink it + // without copying. Just deallocate the lower half. + span = runtime·MHeap_LookupMaybe(&runtime·mheap, oldstk); + if(span == nil) + return; // stack allocated outside heap. Can't shrink it. Can happen if stack is allocated while inside malloc. TODO: shrink by copying? + if(span->elemsize != oldsize) + runtime·throw("span element size doesn't match stack size"); + if((uintptr)oldstk != span->start << PageShift) + runtime·throw("stack not at start of span"); + + if(StackDebug) + runtime·printf("shrinking stack in place %p %X->%X\n", oldstk, oldsize, newsize); + + // new stack guard for smaller stack + gp->stackguard = (uintptr)oldstk + newsize + StackGuard; + gp->stackguard0 = (uintptr)oldstk + newsize + StackGuard; + if(gp->stack0 == (uintptr)oldstk) + gp->stack0 = (uintptr)oldstk + newsize; + + // Free bottom half of the stack. First, we trick malloc into thinking + // we allocated the stack as two separate half-size allocs. Then the + // free() call does the rest of the work for us. + if(oldsize == PageSize) { + // convert span of 1 PageSize object to a span of 2 + // PageSize/2 objects. + span->ref = 2; + span->sizeclass = runtime·SizeToClass(PageSize/2); + span->elemsize = PageSize/2; + } else { + // convert span of n>1 pages into two spans of n/2 pages each. + runtime·MHeap_SplitSpan(&runtime·mheap, span); + } + runtime·free(oldstk); }
コアとなるコードの解説
src/pkg/runtime/malloc.h
void runtime·MHeap_SplitSpan(MHeap *h, MSpan *s);
- この行は、
runtime·MHeap_SplitSpan
関数の前方宣言です。この関数は、mheap.c
で実装され、MSpan
を分割するために使用されます。
- この行は、
src/pkg/runtime/mheap.c
runtime·MHeap_SplitSpan
関数:- この関数は、割り当て済みの
MSpan
s
を受け取り、それを2つの等しい部分に分割します。 - 前提条件チェック:
if((s->npages & 1) != 0)
:MSpan
のページ数(npages
)が奇数の場合、エラーをスローします。これは、正確に半分に分割するためには偶数である必要があるためです。if(s->state != MSpanInUse)
:MSpan
が使用中でない場合、エラーをスローします。if(s->sizeclass != 0 && s->ref != 1)
:MSpan
がサイズクラスを持ち、かつ参照カウントが1でない場合、エラーをスローします。これは、スタックが単一のオブジェクトとして割り当てられていることを確認するためです。
- ロック:
runtime·lock(h)
でヒープをロックし、並行アクセスから保護します。 - 新しい
MSpan
の割り当てと初期化:t = runtime·FixAlloc_Alloc(&h->spanalloc);
: 新しいMSpan
構造体t
を割り当てます。runtime·MSpan_Init(t, s->start, npages/2);
:t
を初期化し、元のMSpan
の開始アドレスとページ数の半分を設定します。t->limit = ...; t->state = MSpanInUse; t->elemsize = ...; t->sweepgen = s->sweepgen;
: 新しいMSpan
のプロパティを設定します。elemsize
は、分割された半分のサイズになります。if(t->elemsize <= MaxSmallSize) { t->sizeclass = runtime·SizeToClass(t->elemsize); t->ref = 1; }
: 新しいMSpan
が小さいオブジェクトを保持できる場合、そのサイズクラスと参照カウントを設定します。
- 元の
MSpan
の更新:s->start += npages/2;
: 元のMSpan
s
の開始アドレスを、後半部分の開始アドレスに更新します。s->npages = npages/2;
: 元のMSpan
のページ数を半分に更新します。s->elemsize = npages << (PageShift - 1);
: 元のMSpan
の要素サイズを更新します。if(s->elemsize <= MaxSmallSize) { s->sizeclass = runtime·SizeToClass(s->elemsize); s->ref = 1; }
: 元のMSpan
が小さいオブジェクトを保持できる場合、そのサイズクラスと参照カウントを設定します。
- スパンルックアップテーブルの更新:
for(i = p; i < p + npages/2; i++) h->spans[i] = t;
: ヒープのspans
配列(アドレスからMSpan
をルックアップするためのテーブル)を更新し、前半部分のアドレス範囲が新しいMSpan
t
を指すようにします。
- アンロック:
runtime·unlock(h)
でヒープのロックを解除します。
- この関数は、割り当て済みの
src/pkg/runtime/stack.c
-
runtime·stackalloc
の変更:if((n & (n-1)) != 0) runtime·throw("stack size not a power of 2");
: 割り当てられるスタックサイズn
が2のべき乗でない場合、ランタイムエラーをスローします。これは、インプレース縮小の前提条件です。
-
round2
関数:static int32 round2(int32 x)
: 与えられた整数x
を、それ以上の最小の2のべき乗に丸めるヘルパー関数です。例えば、round2(5)
は8
を返します。
-
runtime·newstack
の変更:framesize = round2(framesize);
: 新しいスタックのフレームサイズを計算した後、round2
関数を使用して、必ず2のべき乗のサイズになるように丸めます。これにより、すべてのスタックがインプレース縮小の要件を満たすようになります。
-
runtime·shrinkstack
関数の変更:- 縮小条件:
newsize = oldsize / 2;
: 新しいスタックサイズは、現在のスタックサイズの半分です。if(newsize < FixedStack) return;
: 最小スタックサイズFixedStack
より小さくなる場合は縮小しません。if(used >= oldsize / 4) return;
: スタックの使用量が現在のサイズの1/4以上である場合、縮小しません。これは、まだ十分な領域が使用されていると判断されるためです。
- コピーベースの縮小の維持:
if(newsize < PageSize/2) { ... copystack(gp, nframes, newsize); return; }
: 新しいスタックサイズがPageSize/2
(通常2KB)未満の場合、またはsyscallstack
が設定されている場合(Cgo呼び出し中など)、従来のcopystack
関数を使用してスタックをコピーします。これは、非常に小さいスタックへの縮小や、特定のCgoのケースではインプレース縮小が困難または不適切な場合があるためです。
- インプレース縮小のロジック:
span = runtime·MHeap_LookupMaybe(&runtime·mheap, oldstk);
: 古いスタックの開始アドレスoldstk
に対応するMSpan
をヒープから検索します。if(span == nil) return;
: スタックがヒープ外に割り当てられている場合(まれなケース)、インプレース縮小は行われません。if(span->elemsize != oldsize) ... if((uintptr)oldstk != span->start << PageShift) ...
: 取得したMSpan
が実際にスタックに対応しているか、およびスタックがMSpan
の開始位置にあるかを確認します。- スタックガードとスタックベースの更新:
gp->stackguard = (uintptr)oldstk + newsize + StackGuard;
gp->stackguard0 = (uintptr)oldstk + newsize + StackGuard;
if(gp->stack0 == (uintptr)oldstk) gp->stack0 = (uintptr)oldstk + newsize;
- 縮小されたスタックの新しいサイズに合わせて、スタックガードとスタックベースのポインタを更新します。
- スタック下半分の解放:
if(oldsize == PageSize) { ... }
: スタックがちょうど1ページサイズ(通常4KB)の場合。span->ref = 2; span->sizeclass = runtime·SizeToClass(PageSize/2); span->elemsize = PageSize/2;
:MSpan
の参照カウントを2に、サイズクラスと要素サイズを半分のサイズに「騙し」て設定します。これにより、malloc
がこのMSpan
を2つの半分のサイズのオブジェクトとして扱えるようになります。
else { runtime·MHeap_SplitSpan(&runtime·mheap, span); }
: スタックが1ページより大きい場合。runtime·MHeap_SplitSpan
を呼び出して、現在のMSpan
を2つの半分のサイズのMSpan
に分割します。
runtime·free(oldstk);
: 最後に、runtime·free
を呼び出して、スタックの下半分(または、1ページスタックの場合は、malloc
が認識するようになった半分のサイズ)を解放します。これにより、メモリがシステムに返却されます。
- 縮小条件:
この一連の変更により、Goランタイムは、スタックの内容をコピーすることなく、その場でスタックを効率的に縮小できるようになり、特にCgoとの相互運用性における課題を解決し、メモリの再利用を促進します。
関連リンク
- Goのスタック管理に関する公式ドキュメントやブログ記事(コミット当時のもの)
- Goのメモリ管理に関するドキュメント
- GoのCgoに関するドキュメント
参考にした情報源リンク
- golang/go GitHubリポジトリ
- Goのコミット f4359afa7f7886541a51c44cefee39250a202d65
- Go Code Review 69580044 (これはコミットメッセージに記載されているリンクであり、詳細な議論が含まれている可能性があります)
- Goのランタイムソースコード(特に
src/pkg/runtime/
ディレクトリ内のファイル) - Goのメモリ管理やスタックに関する一般的な情報源(ブログ、技術記事など)