Основной принцип Golang — стек сопрограмм

Go

Что такое стек сопрограмм

Каждой сопрограмме требуется собственное пространство стека для хранения переменных, функций, регистров и другой информации. Таким образом, системе необходимо выделить достаточно места в стеке для сопрограммы.

выделение стека

стек фиксированного размера

Каждая сопрограмма имеет одинаковый стек фиксированного размера.

Преимущества: простота реализации;

Недостатки: пространство в стеке, необходимое для каждой сопрограммы, разное.

указывается при создании

Размер стека сопрограммы указывается разработчиком во время создания. Java, C++ могут указывать размер стека при создании потока.

Преимущество: простота реализации

Недостатки: Требования к разработчикам относительно высокие, и необходимо оценивать количество запросов на основе переменных стека. Однако некоторые сценарии не очень легко оценить, например, рекурсивные вызовы, которые обычно можно оценить только в значительной степени.

Segmented stacks

Выделите и интерпретируйте дополнительное пространство памяти. Начальное выделенное пространство относительно невелико, например 4 КБ. Если этого недостаточно, добавьте его снова и отпустите, когда он израсходуется. Вот пример:

Когда G вызывает H, в стеке недостаточно места для запуска H. В это время среда выполнения Go выделит новый блок памяти стека из кучи для запуска H. Вновь выделенный блок памяти освобождается обратно в кучу до того, как H вернется в G. Этот метод управления стеком обычно работает хорошо. Но для некоторого кода, особенно рекурсивных вызовов, это заставит программу постоянно выделять и освобождать новое пространство памяти. Например, в программе функция G будет вызывать функцию H много раз в цикле. Каждый вызов выделяет новую область памяти. Это проблема горячего разделения.

Преимущества: динамическое расширение, низкая начальная стоимость и возможность использования сопрограмм в качестве дешевых ресурсов.

Недостаток: существует проблема горячего разделения.

Stack copying

Динамическое расширение, выделение большего объема памяти, миграция указателей.

Преимущества: динамическое расширение, низкая начальная стоимость, сопрограммы могут использоваться как дешевые ресурсы, и нет проблемы горячего разделения.

Недостатки: Потому что он обычно расширяется в 2 раза, когда объем запросов интенсивный и память чувствительна, памяти будет потребляться больше, и ее легко увеличить.Конечно, обычный бизнес-объем в порядке, и будет Нет проблем. В то же время для оптимизации следует учитывать подключение на 100 Вт.

выделение стека golang

До версии 1.3 использовался метод сегментированных стеков. Копирование стека, используемое позже, также называемое непрерывным стеком (continuous stack).

расширение стека

Время запуска

При запуске обнаруживается, что стека не хватает

ключевой шаг

  1. Обновить статус с _Grunning на _Gcopystack
  2. Рассчитать размер данных, которые необходимо применить
  3. copystack, для копирования стека, который будет подробно проанализирован позже
  4. Восстановить состояние сопрограммы в _Grunning
  5. Пройдите планирование сопрограммы

исходный код ключа

func newstack() {
    thisg := getg()
    ......
    gp := thisg.m.curg
    ......
    // Allocate a bigger segment and move the stack.
    oldsize := gp.stack.hi - gp.stack.lo
    newsize := oldsize * 2 // 比原来大一倍
    ......
    // The goroutine must be executing in order to call newstack,
    // so it must be Grunning (or Gscanrunning).
    casgstatus(gp, _Grunning, _Gcopystack) //修改协程状态

    // The concurrent GC will not scan the stack while we are doing 
    // the copy since the gp is in a Gcopystack status.
    copystack(gp, newsize, true) //在下面会讲到
    ......
    casgstatus(gp, _Gcopystack, _Grunning)
    gogo(&gp.sched)
}

сокращение стека

Время запуска

Когда идет gc, не запущенные сопрограммы, использование стека не превышает 1/4, а емкость будет уменьшена до 1/2 исходной.

ключевой шаг

  1. Проверить состояние сопрограммы, если она закончилась, освободить место
  2. Определите размер нового пространства, которое в настоящее время составляет 1/2 от исходного
  3. Проверить, превышает ли использование стека 1/4, если нет, сдаться
  4. copystack, для копирования стека, который будет подробно проанализирован позже

исходный код ключа

func shrinkstack(gp *g) {
    gstatus := readgstatus(gp)
    if gstatus&^_Gscan == _Gdead {
	    if gp.stack.lo != 0 {
	        // Free whole stack - it will get reallocated
	        // if G is used again.
	        stackfree(gp.stack)
	        gp.stack.lo = 0
	        gp.stack.hi = 0
	    }
	    return
    }
    ......
    oldsize := gp.stack.hi - gp.stack.lo
    newsize := oldsize / 2 // 比原来小1倍
    if newsize < _FixedStack {
        return
    }
    // Compute how much of the stack is currently in use and only
    // shrink the stack if gp is using less than a quarter of its
    // current stack. The currently used stack includes everything
    // down to the SP plus the stack guard space that ensures
    // there's room for nosplit functions.
    avail := gp.stack.hi - gp.stack.lo
    //当已使用的栈占不到总栈的1/4 进行缩容
    if used := gp.stack.hi - gp.sched.sp + _StackLimit; used >= avail/4 {
        return
    }

    copystack(gp, newsize, false) //在下面会讲到
}

процесс копирования стека копий

копия оригинального контента

ключевой шаг

  1. Подать заявку на новое пространство стека: new := stackalloc(uint32(newsize));
  2. Отрегулируйте указатель указателя, укажите sudog, ctx и т. Д. На новое местоположение, метод расчета - исходный адрес + дельта (дельта - это new.hi-old.hi);
  3. gentraceback, настроить фрейм стека на новую позицию;
  4. memmove старые данные стека в новый стек;
  5. Удалить старый стек.
func copystack(gp *g, newsize uintptr, sync bool) {
    ......
    old := gp.stack
    ......
    used := old.hi - gp.sched.sp

    // allocate new stack
    new := stackalloc(uint32(newsize))
    ......
    // Compute adjustment.
    var adjinfo adjustinfo
    adjinfo.old = old
    adjinfo.delta = new.hi - old.hi //用于旧栈指针的调整

    //后面有机会和 select / chan 一起分析
    // Adjust sudogs, synchronizing with channel ops if necessary.
    ncopy := used
    if sync {
        adjustsudogs(gp, &adjinfo)
    } else {
        ......
        adjinfo.sghi = findsghi(gp, old)

        // Synchronize with channel ops and copy the part of
        // the stack they may interact with.
        ncopy -= syncadjustsudogs(gp, used, &adjinfo)
    }
    //把旧栈数据复制到新栈
    // Copy the stack (or the rest of it) to the new location
    memmove(unsafe.Pointer(new.hi-ncopy), unsafe.Pointer(old.hi-ncopy), ncopy)

    // Adjust remaining structures that have pointers into stacks.
    // We have to do most of these before we traceback the new
    // stack because gentraceback uses them.
    adjustctxt(gp, &adjinfo)
    adjustdefers(gp, &adjinfo)
    adjustpanics(gp, &adjinfo)
    ......
    // Swap out old stack for new one
    gp.stack = new
    gp.stackguard0 = new.lo + _StackGuard // NOTE: might clobber a preempt request
    gp.sched.sp = new.hi - used
    gp.stktopsp += adjinfo.delta
    // Adjust pointers in the new stack.
    gentraceback(^uintptr(0), ^uintptr(0), 0, gp, 0, nil, 0x7fffffff, adjustframe, noescape(unsafe.Pointer(&adjinfo)), 0)
    ......
    //释放旧栈
    stackfree(old)
}

регулировка кадра стека

рамка стека голанга
package main

func myFunction(a, b int) (int, int) {
    return a + b, a - b
}

func main() {
    myFunction(66, 77)
}

регулировка кадра стека

Функция Adjustframe вызывается обратно в gentraceback. Нам нужно знать, что пространство стека golang содержит такую ​​информацию, как параметры функции, возвращаемые значения и адреса возврата функции. Эти адреса необходимо скорректировать. Эта функция предназначена для настройки исходный указатель стека. код показывает, как показано ниже:

// Note: the argument/return area is adjusted by the callee.
func adjustframe(frame *stkframe, arg unsafe.Pointer) bool {
	adjinfo := (*adjustinfo)(arg)
	targetpc := frame.continpc
	if targetpc == 0 {
		// Frame is dead.
		return true
	}
	f := frame.fn
    .........
	pcdata := pcdatavalue(f, _PCDATA_StackMapIndex, targetpc, &adjinfo.cache)
	if pcdata == -1 {
		pcdata = 0 // in prologue
	}

	// Adjust local variables if stack frame has been allocated.
	size := frame.varp - frame.sp
	var minsize uintptr
	switch sys.ArchFamily {
	case sys.ARM64:
		minsize = sys.SpAlign
	default:
		minsize = sys.MinFrameSize
	}
	if size > minsize {
		var bv bitvector
		stackmap := (*stackmap)(funcdata(f, _FUNCDATA_LocalsPointerMaps))
		if stackmap == nil || stackmap.n <= 0 {
			print("runtime: frame ", funcname(f), " untyped locals ", hex(frame.varp-size), "+", hex(size), "\n")
			throw("missing stackmap")
		}
		// Locals bitmap information, scan just the pointers in locals.
		if pcdata < 0 || pcdata >= stackmap.n {
			print("runtime: pcdata is ", pcdata, " and ", stackmap.n, " locals stack map entries for ", funcname(f), " (targetpc=", targetpc, ")\n")
			throw("bad symbol table")
		}
		bv = stackmapdata(stackmap, pcdata)
		size = uintptr(bv.n) * sys.PtrSize
		if stackDebug >= 3 {
			print("      locals ", pcdata, "/", stackmap.n, " ", size/sys.PtrSize, " words ", bv.bytedata, "\n")
		}
		adjustpointers(unsafe.Pointer(frame.varp-size), &bv, adjinfo, f)
	}

	// Adjust saved base pointer if there is one.
	if sys.ArchFamily == sys.AMD64 && frame.argp-frame.varp == 2*sys.RegSize {
		if !framepointer_enabled {
			print("runtime: found space for saved base pointer, but no framepointer experiment\n")
			print("argp=", hex(frame.argp), " varp=", hex(frame.varp), "\n")
			throw("bad frame layout")
		}
		if stackDebug >= 3 {
			print("      saved bp\n")
		}
		if debugCheckBP {
			// Frame pointers should always point to the next higher frame on
			// the Go stack (or be nil, for the top frame on the stack).
			bp := *(*uintptr)(unsafe.Pointer(frame.varp))
			if bp != 0 && (bp < adjinfo.old.lo || bp >= adjinfo.old.hi) {
				println("runtime: found invalid frame pointer")
				print("bp=", hex(bp), " min=", hex(adjinfo.old.lo), " max=", hex(adjinfo.old.hi), "\n")
				throw("bad frame pointer")
			}
		}
		adjustpointer(adjinfo, unsafe.Pointer(frame.varp))
	}

	// Adjust arguments.
	if frame.arglen > 0 {
		var bv bitvector
		if frame.argmap != nil {
			bv = *frame.argmap
		} else {
			stackmap := (*stackmap)(funcdata(f, _FUNCDATA_ArgsPointerMaps))
			if stackmap == nil || stackmap.n <= 0 {
				print("runtime: frame ", funcname(f), " untyped args ", frame.argp, "+", frame.arglen, "\n")
				throw("missing stackmap")
			}
			if pcdata < 0 || pcdata >= stackmap.n {
				print("runtime: pcdata is ", pcdata, " and ", stackmap.n, " args stack map entries for ", funcname(f), " (targetpc=", targetpc, ")\n")
				throw("bad symbol table")
			}
			bv = stackmapdata(stackmap, pcdata)
		}
		if stackDebug >= 3 {
			print("args\n")
		}
		adjustpointers(unsafe.Pointer(frame.argp), &bv, adjinfo, funcInfo{})
	}
	return true
}