Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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関数を使用してスタックを縮小していました。これは、より小さい新しいスタック領域を割り当て、古いスタックの内容を新しい領域にコピーし、その後古いスタック領域を解放するというプロセスでした。

このコピーベースの縮小にはいくつかの問題がありました。

  1. パフォーマンスオーバーヘッド: 大量のデータをコピーする必要があるため、パフォーマンスに影響を与える可能性がありました。
  2. Cgoとの相互作用: GoのゴルーチンがCコードを呼び出し、そのCコード内でブロックしている場合、Goランタイムがスタックをコピーしようとすると、CスタックフレームとGoスタックフレームの間の整合性が失われ、問題が発生する可能性がありました。特に、CコードがGoスタック上のポインタを保持している場合、スタックの移動は危険でした。
  3. メモリの再利用の遅延: Cコード内でブロックしているゴルーチンが、以前に大量のスタックを消費していた場合、そのスタック領域はすぐに解放されず、メモリの効率的な再利用が妨げられる可能性がありました。

このコミットの背景にあるのは、これらの課題を解決し、より効率的で安全なスタック縮小メカニズムを提供することです。特に、Cコードとの相互運用性を向上させ、メモリの再利用を促進することが重要な目標でした。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念とメモリ管理の仕組みについて理解しておく必要があります。

  1. ゴルーチンスタック:

    • Goのゴルーチンは、OSのスレッドとは異なり、非常に小さい初期スタック(通常は数KB)で開始します。
    • 関数呼び出しが深くなるにつれて、必要に応じてスタックは自動的に拡大します。
    • 関数から戻るなどしてスタックの使用量が減ると、スタックは縮小される可能性があります。
    • スタックは連続したメモリ領域として割り当てられます。
  2. スタックガード (StackGuard):

    • Goのスタックは、スタックオーバーフローを検出するために「スタックガード」と呼ばれるメカニズムを使用します。
    • スタックの末尾近くにガードページが設定されており、スタックポインタがこのガードページに到達すると、ランタイムがスタックの拡大処理(または縮小処理)を開始します。
  3. メモリヒープ (MHeap):

    • Goランタイムは、独自のメモリマネージャを持っています。MHeapは、Goプログラムが使用するすべてのメモリを管理するグローバルなヒープです。
    • MHeapは、メモリをMSpanと呼ばれる大きなチャンクに分割して管理します。
  4. MSpan:

    • MSpanは、Goランタイムのメモリ管理における基本的な単位です。これは、連続したページ(PageSizeの倍数)のブロックを表します。
    • MSpanは、オブジェクトの割り当てやスタックの割り当てに使用されます。
    • MSpanには、その状態(使用中、空きなど)、ページ数、サイズクラスなどの情報が含まれます。
  5. ページサイズ (PageSize):

    • Goランタイムは、OSのメモリページサイズ(通常は4KB)を基盤としてメモリを管理します。PageSizeは、メモリ割り当ての最小単位です。
  6. FixedStack:

    • Goのゴルーチンスタックには、FixedStackという最小サイズがあります。スタックはこれ以上小さくはなりません。
  7. copystack:

    • このコミット以前のGoランタイムでスタックを縮小する際に使用されていた関数です。
    • 新しい小さいスタック領域を割り当て、古いスタックの内容を新しい領域にコピーし、古いスタックを解放するという動作をしていました。
  8. 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の変更: 新しいスタックを割り当てる際に、framesizeround2関数で丸めるように変更されました。これにより、常に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関数は、ゴルーチンのスタックを縮小する主要な関数であり、このコミットで最も大きな変更が加えられました。

  • コピーベースの縮小の条件付き維持: newsizePageSize/2(通常2KB)未満の場合、またはsyscallstackが設定されている場合(Cgo呼び出し中など)、依然としてcopystackを使用してスタックをコピーする動作が残されています。これは、非常に小さいスタックへの縮小や、特定のCgoのケースではインプレース縮小が困難または不適切な場合があるためです。
  • インプレース縮小のロジック:
    • スタックの基盤となるMSpanruntime·MHeap_LookupMaybeで検索します。
    • スタックがヒープ外に割り当てられている場合(まれなケース)、インプレース縮小は行われません。
    • スタックが1ページサイズ(通常4KB)の場合、MSpanrefsizeclasselemsizeを直接変更することで、あたかも2つの半分のサイズのオブジェクトとして割り当てられたかのように「騙し」、下半分を解放できるようにします。
    • スタックが1ページより大きい場合、runtime·MHeap_SplitSpanを呼び出してMSpanを2つに分割し、下半分を構成するMSpanを独立して解放できるようにします。
    • 新しいスタックガード(stackguardstackguard0)とスタックベース(stack0)が、縮小されたスタックの新しいサイズに合わせて更新されます。
    • 最後に、runtime·free(oldstk)を呼び出すことで、スタックの下半分が解放されます。

利点

この新しいインプレース縮小メカニズムは、以下の重要な利点をもたらします。

  • Cgoとの互換性向上: スタックをコピーする必要がなくなるため、Cコード内でブロックしているゴルーチンのスタックを安全に縮小できるようになります。これにより、Cgoを使用するアプリケーションの安定性とパフォーマンスが向上します。
  • メモリの効率的な再利用: 過剰に確保されたスタック領域が迅速に解放されるため、メモリの断片化が減少し、全体的なメモリ使用効率が向上します。
  • パフォーマンスの向上: スタックコピーのオーバーヘッドがなくなるため、スタック縮小のパフォーマンスが向上します。

このコミットは、Goランタイムのメモリ管理、特にスタック管理の堅牢性と効率性を大幅に向上させる重要な一歩と言えます。

コアとなるコードの変更箇所

このコミットにおける主要なコード変更は、以下の3つのファイルに集中しています。

  1. 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);
    
  2. 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);
    +}
    
  3. src/pkg/runtime/stack.c:

    • runtime·stackallocに、スタックサイズが2のべき乗であることのチェックが追加されました。
    • round2ヘルパー関数が追加されました。
    • runtime·newstackframesizeround2で丸めるように変更されました。
    • 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に関するドキュメント

参考にした情報源リンク