Подробно - что такое пустая структура?

Go

Публичный номер: облачное хранилище Qiya

[toc]

задний план

голанг нормальныйstructЭто обычный блок памяти, который должен занимать небольшой кусок памяти, причем размер структуры должен проходить через границу и длина выравнивается, но "пустая структура" памяти не занимает, а размер равен 0;

Совет: Следующие данные основаны на анализе go1.13.3 linux/amd64.

Общая структура определяется следующим образом:

// 类型变量对齐到 8 字节;
type Tp struct {
    a uint16
    b uint32
}

Согласно правилам выравнивания памяти, эта структура занимает 8 байт памяти.

Пустая структура:

var s struct{}
// 变量 size 是 0 ;
fmt.Println(unsafe.Sizeof(s))

Переменные этой пустой структуры занимают 0 байт памяти.

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

Споры

Специальная переменная: нулевое основание

Пустая структура — это структура без размера памяти. Это предложение не является неправильным, но, если быть более точным, оно действительно имеет особую отправную точку, т.zerobaseпеременная, которая являетсяuintptr Глобальная переменная, занимающая 8 байт. Когда бесконечное количество определенных где угодноstruct {}переменные типа, компилятор просто помещает этоzerobaseУказан адрес переменной. Другими словами, в golang задействованы все выделения памяти с размером памяти 0, тогда используется один и тот же адрес&zerobase.

Например:

package main

import "fmt"

type emptyStruct struct {}

func main() {
	a := struct{}{}
	b := struct{}{}
	c := emptyStruct{}

	fmt.Printf("%p\n", &a)
	fmt.Printf("%p\n", &b)
	fmt.Printf("%p\n", &c)
}

Отладка и анализ dlv:

(dlv) p &a
(*struct {})(0x57bb60)
(dlv) p &b
(*struct {})(0x57bb60)
(dlv) p &c
(*main.emptyStruct)(0x57bb60)
(dlv) p &runtime.zerobase
(*uintptr)(0x57bb60)

Резюме: Адреса памяти переменных пустой структуры одинаковы.

Особая обработка управления памятью

mallocgc

Во время компиляции компилятор распознает, чтоstruct {}Этот особый тип выделения памяти будет выделять всеruntime.zerobaseАдрес выходит, эта логика кода находится вmallocgcВнутри функции:

код показывает, как показано ниже:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // 分配 size 为 0 的结构体,把全局变量 zerobase 的地址给出去即可;
	if size == 0 {
		return unsafe.Pointer(&zerobase)
	}
    // ... 

Резюме: golang используетmallocgcПри выделении памяти, если размер равен 0, все глобальные переменные возвращаются единообразноzerobaseадрес г.

Наличие этого глобального уникального специального адреса также удобно для специальной обработки некоторой логики позже.

Определены различные позы

родное определение

a := struct{}{}

struct{}Его можно рассматривать как тип, а переменнуюstruct {}Переменная типа с адресомruntime.zerobase, размер равен 0 и не занимает память.

переопределить тип

голанг использоватьtypeКлючевые слова определяют новые типы, такие как:

type emptyStruct struct{}

определенныйemptyStructэто новый тип с соответствующимtypeструктура, но природаstruct{}Точно так же, компилятор дляemptryStructТипы распределения памяти также задаются непосредственноzerobaseадрес.

анонимный вложенный тип

struct{}В качестве анонимного поля встраивайте другие структуры. Какова эта ситуация?

Анонимный метод вложения один

type emptyStruct struct{}
type Object struct {
    emptyStruct
}

Анонимный метод вложения 2

type Object1 struct {
    _ struct {}
}

Помните, что пустая структура остается пустой структурой, а сама переменная типа абсолютно не выделяет память ( size=0 ), поэтому компилятор неObject,Object1Обработка двух типов согласуется с типом пустой структуры, а адрес распределенияruntime.zerobaseАдрес, переменный размер 0, не занимает никакого размера памяти.

Встроенное поле

В сцене встроенных полей нет ничего особенного, в основном следует учитывать выравнивание адреса и длины. Еще просто нужно обратить внимание на 3 момента:

  • Тип пустой структуры не занимает объем памяти;
  • Смещение адреса должно быть выровнено с собственным типом;
  • Общая длина типа должна быть выровнена с максимальной длиной типа поля;

Мы обсуждаем этот вопрос в 3 сценариях:

сцена первая:struct {}на передней

Этот сценарий очень понятен,struct {}Тип поля находится вверху, и этот тип не занимает места, поэтому естественно адрес второго поля совпадает с адресом всей переменной.

// Object1 类型变量占用 1 个字节
type Object1 struct {
	s struct {}
	b byte
}

// Object2 类型变量占用 8 个字节
type Object2 struct {
	s struct {}
	n int64
}

o1 := Object1{ }
o2 := Object2{ }

Как распределяется память?

  • &o1а также&o1.sнепротиворечива, переменнаяo1Размер памяти выровнен до 1 байта;
  • &o2а также&o2.sнепротиворечива, переменнаяo2Размер памяти выравнивается до 8 байт;

Это распределение удовлетворяет правилам выравнивания, и компилятор не будетstruct {}Поля выполняют любое специальное заполнение байтами.

Сценарий второй:struct {}в середине

// Object1 类型变量占用 16 个字节
type Object1 struct {
	b  byte
	s  struct{}
	b1 int64
}

o1 := Object1{ }
  • Согласно правилам выравнивания, переменнаяo1Занимает 16 байт;
  • &o1.sа также&o1.b1такой же;

компилятор неstruct { }Выполните любое заполнение байтов.

Сценарий третий:struct {}в конце

Обратите внимание на эту сцену, потому что компилятор выполнит специальное заполнение байтов после ее обнаружения следующим образом;

type Object1 struct {
	b byte
	s struct{}
}

type Object2 struct {
	n int64
	s struct{}
}

type Object3 struct {
	n int16
	m int16
	s struct{}
}

type Object4 struct {
	n  int16
	m  int64
	s  struct{}
}

o1 := Object1 { }
o2 := Object2 { }
o3 := Object3 { }
o4 := Object4 { }

Компилятор сталкивается с этимstruct {}существуетпоследнее полесцена, будет выполнено специальное заполнение,struct { }Как последнее поле, оно будет заполнено и выровнено по размеру предыдущего поля, а правила выравнивания смещения адреса останутся без изменений;

Подумайте об этом сейчас,o1,o2,o3,o4Сколько места занимает выделение памяти для этих четырех объектов? Расшифруйте ниже:

  • Переменнаяo1размер 2 байта;
  • Переменнаяo2Размер 16 байт;
  • Переменнаяo3размер 6 байт;
  • Переменнаяo4размер 24 байта;

В этом случае необходимо сначалаstruct {}Память для заполнения выделяется в соответствии с длиной предыдущего поля, а затем вся переменная остается неизменной в соответствии с правилами выравнивания адреса и длины.

struct {}как приемник

ресивер Это основная функция структуры в golang. Пустые структуры по существу аналогичны структурам и могут использоваться в качестве получателей для определения методов.

type emptyStruct struct{}

func (e *emptyStruct) FuncB(n, m int) {
}
func (e emptyStruct) FuncA(n, m int) {
}

func main() {
	a := emptyStruct{}

	n := 1
	m := 2

	a.FuncA(n, m)
	a.FuncB(n, m)
}

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

func FuncA (e *emptyStruct, n, m int) {
}
func FuncB (e  emptyStruct, n, m int) {
}

Компилятор просто передает значение или адрес объекта в качестве первого параметра этому параметру, это так просто.Но здесь следует отметить, что пустая структура немного отличается, пустая структура должна быть переведена в:

func FuncA (e *emptyStruct, n, m int) {
}
func FuncB (n, m int) {
}

Чрезвычайно простой код, соответствующий фактический ассемблерный код выглядит следующим образом:

FuncA, FuncB так же просты, как это:

00000000004525b0 <main.(*emptyStruct).FuncB>:
  4525b0:	c3                   	retq   

00000000004525c0 <main.emptyStruct.FuncA>:
  4525c0:	c3                   	retq    

основная функция

00000000004525d0 <main.main>:
  4525d0:	64 48 8b 0c 25 f8 ff 	mov    %fs:0xfffffffffffffff8,%rcx
  4525d9:	48 3b 61 10          	cmp    0x10(%rcx),%rsp
  4525dd:	76 63                	jbe    452642 <main.main+0x72>
  4525df:	48 83 ec 30          	sub    $0x30,%rsp
  4525e3:	48 89 6c 24 28       	mov    %rbp,0x28(%rsp)
  4525e8:	48 8d 6c 24 28       	lea    0x28(%rsp),%rbp
  4525ed:	48 c7 44 24 18 01 00 	movq   $0x1,0x18(%rsp)
  4525f6:	48 c7 44 24 20 02 00 	movq   $0x2,0x20(%rsp)
  4525ff:	48 8b 44 24 18       	mov    0x18(%rsp),%rax
  452604:	48 89 04 24          	mov    %rax,(%rsp)			// n 变量值压栈(第一个参数)
  452608:	48 c7 44 24 08 02 00 	movq   $0x2,0x8(%rsp)		// m 变量值压栈(第二个参数)
  452611:	e8 aa ff ff ff       	callq  4525c0 <main.emptyStruct.FuncA>
  452616:	48 8d 44 24 18       	lea    0x18(%rsp),%rax
  45261b:	48 89 04 24          	mov    %rax,(%rsp)			// $rax 里面是 zerobase 的值,压栈(第一个参数);
  45261f:	48 8b 44 24 18       	mov    0x18(%rsp),%rax
  452624:	48 89 44 24 08       	mov    %rax,0x8(%rsp)		// n 变量值压栈(第二个参数)
  452629:	48 8b 44 24 20       	mov    0x20(%rsp),%rax
  45262e:	48 89 44 24 10       	mov    %rax,0x10(%rsp)		// m 变量值压栈(第三个参数)
  452633:	e8 78 ff ff ff       	callq  4525b0 <main.(*emptyStruct).FuncB>
  452638:	48 8b 6c 24 28       	mov    0x28(%rsp),%rbp
  45263d:	48 83 c4 30          	add    $0x30,%rsp
  452641:	c3                   	retq   
  452642:	e8 b9 7a ff ff       	callq  44a100 <runtime.morestack_noctxt>
  452647:	eb 87                	jmp    4525d0 <main.main>

Этот код подтверждает несколько моментов:

  1. Получатель на самом деле является своего рода синтаксическим сахаром, который по существу передается в функцию в качестве первого параметра;
  2. Когда получатель является значением, нет необходимости передавать пустую структуру в качестве первого параметра, потому что пустая структура не имеет значения;
  3. Когда получатель является указателем, адрес объекта передается в функцию в качестве первого параметра.При вызове функции компилятор передаетzerobaseЗначение (может быть подтверждено при компиляции);

Обычно после бинарной компиляцииe.FuncAЗвоните, первый аргумент подталкивается напрямую&zerobaseв стек.

Обобщите несколько пунктов знаний:

  • Получатель — это, по сути, очень простая общая идея, заключающаяся в передаче значения объекта или адреса в качестве первого параметра функции;
  • Метод стека параметров функции — спереди назад (вы можете отладить его);
  • Когда значение объекта используется в качестве приемника, задействована копия значения;
  • Для определения функции получателя значения golang может сгенерировать две функции в соответствии с фактическими потребностями, версию значения и версию указателя (подумав: что такое «требование»?interfaceместо действия);
  • Для сценариев, где во время компиляции может быть идентифицирована пустая структура, компилятор может выполнить специальную генерацию кода для заданных фактов;

Можно сказать, что во время компиляции в основном могут быть определены параметры пустой структуры, поэтому при генерации кода может быть сгенерирован соответствующий статический код.

использовать позу

пустая структураstruct{ }Основная причина, по которой он существует, состоит в том, чтобысохранить память. Когда вам нужна структура, но не важно, что внутри, рассмотрите пустую структуру. Несколько составных структур ядра голангаmap,chan,sliceможно комбинироватьstruct{}использовать.

map & struct{}

mapа такжеstruct {}Общая поза соединения выглядит следующим образом:

// 创建 map
m := make(map[int]struct{})
// 赋值
m[1] = struct{}{}
// 判断 key 键存不存在
_, ok := m[1]

в общемmapа такжеstruct {}Комбинированный сценарий использования: заботиться только о ключе, а не о значении. Например, вы можете использовать эту структуру данных, чтобы узнать, существует ли ключ.okзначение, чтобы определить, существует ли ключ,mapСложность запроса O(1), запрос быстрый.

Вы, конечно, можете использоватьmap[int]boolВместо этого типа функция также может быть достигнута, многие люди рассматривают использованиеmap[int]struct{}Этот способ использования действительно для экономии памяти, конечно, в большинстве случаев эта экономия незначительна, поэтому использовать ли его таким образом, зависит от конкретной сцены.

chan & struct{}

channelа такжеstruct{}Комбинация - одна из самых классических сцен,struct{}Обычно передается как сигнал, не обращая внимания на его содержание. Анализ чан подробно описан в предыдущих статьях. Существенной структурой данных chan является структура управления плюс кольцевой буфер, еслиstruct{}В качестве элемента кольцевому буферу выделяется 0.

chanа такжеstruct{}В основном существует только одно использование комбинации, котороесигнализацияСама пустая структура не может нести значения, поэтому существует только такого использования, в общем, с использованием канала отсутствия буфера.

// 创建一个信号通道
waitc := make(chan struct{})

// ...
goroutine 1:
    // 发送信号: 投递元素
    waitc <- struct{}
    // 发送信号: 关闭
    close(waitc)

goroutine 2:
    select {
    // 收到信号,做出对应的动作
    case <-waitc:
    }    

В этом сценарии давайте подумаем о том, должно ли это правильно или неправильноstruct{}Не можешь? На самом деле нет, и памяти этих нескольких байт не так уж и много, так что эта ситуация на самом деле просто не волнуетchanЭто только значение элемента , поэтому оно используетсяstruct{}.

slice & struct{}

формальный,sliceтакже в сочетанииstruct{}.

s := make([]struct{}, 100)

Мы создаем массив, который занимает всего 24 байта памяти (addr, len, cap) независимо от того, насколько большой он выделен, но, честно говоря, такое использование не очень полезно.

Создание среза фактически вызываетmakesliceвыделять память, которая называетсяmalllocgc,а такжеmallocgcМы знаем, что при выделении памяти с размером 0 возвращается сразуzerobaseтолько адрес. Когда слайс расширяется, когда он встречает такой размер 0, он также напрямую возвращает адрес нулевой базы.

func growslice(et *_type, old slice, cap int) slice {
    // 如果元素的 size 为 0,那么还是直接赋值了 zerobase 的地址;
    if et.size == 0 {
        return slice{unsafe.Pointer(&zerobase), old.len, cap}
    }
}

Суммировать

  1. Пустая структура также является структурой, только типом размера 0;
  2. Все пустые структуры имеют общий адрес:zerobaseадрес;
  3. Пустая структура может использоваться в качестве приемника, когда приемник является пустой структурой в качестве значения, компилятор фактически игнорирует передачу первого параметра, и компилятор может подтвердить, что соответствующий код генерируется во время компиляции;
  4. mapа такжеstruct{}Комбинированное использование часто используется для экономии памяти, а используемая сцена обычно используется для определения того, существует ли ключ вmap;
  5. chanа такжеstruct{}Комбинированное использование обычно используется для синхронизации сигналов.Намерение не состоит в том, чтобы экономить память, но мы действительно не заботимся о значении элемента chan;
  6. sliceа такжеstruct{}Комбинировать, похоже, не получится. . .

Публичный номер: облачное хранилище Qiya