Доброе утро/день/вечер/ночь!
Меня зовут Рома и это вторая часть цикла «Типы и структуры данных Go». В этот раз будем разбираться с внутренностями строк. В первой части этого цикла я уже говорил, что какое-то время назад я захотел изучить всю внутрянку Go, заглянуть в исходники языка и понять, почему все устроено так, как устроено. В этот самый момент я обнаружил, что на просторах интернета практически отсутствуют материалы, которые подробно разбирают типы данных, их вспомогательные функции, детали реализации рантайма и так далее. Поэтому мной было принято решение сделать это самостоятельно. Изначально я занимался этим для себя, но позже решил, что стоит поделиться моими наблюдениями и выводами с миром.
Говорить будем только о том, что есть в базе языка, то есть без дополнительных функций из стандартной, экспериментальной или какой-либо другой библиотеки, так как рассмотреть настолько большой массив информации было бы слишком тяжело.
Приятного аппетита!
Строки
Очень важно разделять представление в рантайме Go ИНФОРМАЦИИ/МЕТАДАННЫХ и ДАННЫХ типа, потому что это два разных уровня абстракции, каждый из которых играет свою роль в системе типов. Ниже поговорим о ИНФОРМАЦИИ/МЕТАДАННЫХ СТРОК.
ИНФОРМАЦИЯ/МЕТАДАННЫЕ — это цепочка описательных структур. Эта цепочка существует в одном экземпляре на тип. Она используется для того, чтобы понять сколько байт занимает значение типа, как копировать, сравнивать и очищать тип, какие поля или элементы тип содержит, где в памяти расположены указатели на тип (для сборщика мусора).
Изучим звенья этой цепочки по порядку.
→ rtype из go/src/runtime/type.go — структура, описывающая ИНФОРМАЦИЮ/МЕТАДАННЫЕ о любом типе Go в рантайме.
type rtype struct { *abi.Type }
→ Type, встроенная в rtype структура, из go/src/internal/abi/type.go содержит поля для хранение ИНФОРМАЦИИ/МЕТАДАННЫХ о типе Go.
type Type struct { Size_ uintptr // Размер объекта данного типа в байтах PtrBytes uintptr // Количество начальных байт, содержащих указатели (для сборщика мусора) Hash uint32 // Хеш типа; используется для ускорения в хеш-таблицах TFlag TFlag // Дополнительные флаги информации о типе Align_ uint8 // Выравнивание переменной этого типа FieldAlign_ uint8 // Выравнивание поля в структуре с этим типом Kind_ Kind // Вид типа (enum), совместим с C // Функция сравнения объектов данного типа // принимает (указатель на объект A, указатель на объект B) → возвращает, равны ли они Equal func(unsafe.Pointer, unsafe.Pointer) bool // GCData содержит данные для сборщика мусора. // Обычно это указатель на битовую маску, описывающую, // какие поля типа являются указателями (ptr) или нет (nonptr). // Маска содержит как минимум PtrBytes / ptrSize бит. // Если установлен флаг TFlagGCMaskOnDemand, то GCData — это // **byte, и указатель на битмаску находится на один уровень разыменования глубже. // В этом случае рантайм сам построит маску при необходимости. // (см. runtime/type.go:getGCMask) // Примечание: разные типы могут иметь одинаковое значение GCData, // включая случаи с TFlagGCMaskOnDemand. Такие типы, конечно же, // будут иметь одинаковое размещение указателей (но не обязательно одинаковый размер). GCData *byte Str NameOff // Смещение к строковому представлению типа PtrToThis TypeOff // Смещение к типу указателя на данный тип, может быть нулём }
→ Kind_ типа Kind внутри Type из go/src/internal/abi/type.go содержит uint8 (enum) обозначение типа Go.
→ Kind из go/src/internal/abi/type.go — кастомный тип, объединяющий константные обозначения типов Go.
type Kind uint8 const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Pointer Slice String Struct UnsafePointer )
Перейдем к представлению ДАННЫХ СТРОК в рантайме Go. Это будет маленький блок, потому что рассказывать особо не о чем.
ДАННЫЕ — это фактические данные типа, с которыми работает программа во время выполнения.
→ stringStruct из go/src/runtime/string.go — структура из двух полей, содержащая ДАННЫЕ СТРОКИ.
type stringStruct struct { str unsafe.Pointer // Указатель на первый байт len int // Длина строки в байтах }
Теперь, когда мы разобрались с представлениями строки в рантайме, пришло время поговорить о операциях связанных с этим типом и том, как они реализованы.
Сначала я буду давать небольшие выдержки из книги «The Go Programming Language» для общего понимания, как та или иная операция работает, а затем приводить подробный разбор внутренней реализации.
Доступ к байтам
«The Go Programming Language»
Операция извлечения подстроки s[i:j] возвращает новую строку, состоящую из байтов оригинальной строки, начиная с индекса i и заканчивая (но не включая) байтом с индексом j. Результат содержит j—i байт:
fmt.Println(s[0:5]) // "hello"
Опять же, паника произойдет, если любой из индексов выходит за границы или если j < i.
Один или оба операнда i и j могут быть опущены, в этом случае предполагаются значения по умолчанию: 0 — начало строки и len(s) — конец строки:
fmt.Println(s[:5]) // "hello" fmt.Println(s[7:]) // "world" fmt.Println(s[:]) // "hello, world"
Извлечение подстроки (s[i:j]) — встроенная операция компилятора, реализованная без вызова функции. Память не копируется — создаётся новая “ссылка” на диапазон байт. Код проверяет границы, смещает указатель и задаёт новую длину.
Сравнение строк
«The Go Programming Language»
Строки можно сравнивать с помощью операторов сравнения, таких как == и <; сравнение выполняется побайтово, так что результат соответствует лексикографическому порядку.
Конкретная реализация может быть на Go или на ассемблере, в зависимости от архитектуры. Например, для arm64/x86 рассмотрена оптимизация через memequal или специальные ассемблерные расширения. В коде используется подход: cначала сравниваются длины строк, потом — их содержимое через memequal.
Конкатенация
«The Go Programming Language»
Оператор + создает новую строку путем конкатенации двух строк:
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
Если конкатенируются строковые литералы, Go-компилятор сразу объединяет их на этапе компиляции. Если строк больше одной и они переменные, компилятор использует функцию рантайма concatstrings.
func concatstrings(buf *tmpBuf, a []string) string { idx := 0 l := 0 count := 0 for i, x := range a { n := len(x) if n == 0 { continue } if l+n < l { throw("string concatenation too long") } l += n count++ idx = i } if count == 0 { return "" } if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) { return a[idx] } s, b := rawstringtmp(buf, l) for _, x := range a { n := copy(b, x) b = b[n:] } return s }
Преобразования
«The Go Programming Language»
Строки можно преобразовывать в срезы байтов:
s := "abc" b := []byte(s) s2 := string(b)
Концептуально, преобразование []byte(s) выделяет новый массив байтов, содержащий копию байтов строки s, и возвращает срез, ссылающийся на весь этот массив. Оптимизирующий компилятор может избежать выделения памяти и копирования в некоторых случаях, но в общем случае копирование необходимо, чтобы гарантировать неизменяемость байтов s, даже если байты b будут впоследствии изменены. Преобразование из байтового среза обратно в строку через string(b) тоже делает копию, чтобы результат — строка s2 — оставалась неизменяемой.
Срезы байтов можно преобразовывать в строки:
s := "abc" b := []byte(s) s2 := string(b)
Преобразование из байтового среза обратно в строку через string(b) делает копию, чтобы результат — строка s2 — оставалась неизменяемой.
-
Преобразование из
stringв[]byteреализовано через функциюstringtoslicebyte(внутриgo/src/runtime/string.go).stringtoslicebyteсоздаёт копию содержимого строки и возвращает срез[]byte.func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { *buf = tmpBuf{} b = buf[:len(s)] } else { b = rawbyteslice(len(s)) } copy(b, s) return b } -
Преобразование из
[]byteвstringреализовано через функциюslicebytetostring(внутриgo/src/runtime/string.go).slicebytetostringсоздаёт новую строку, копируя байты из среза.// slicebytetostring преобразует срез байт в строку. // Эта функция вставляется компилятором в сгенерированный код. // ptr — указатель на первый элемент среза; // n — длина среза. // buf — буфер фиксированного размера для результата, // он не равен nil, если результат не утекает из функции (escape analysis). func slicebytetostring(buf *tmpBuf, ptr *byte, n int) string { if n == 0 { // Это оказывается довольно частым случаем. // Например, когда вы хотите выделить данные между скобками в строке "foo()bar", // вы находите индексы и преобразуете подстроку в строку. return "" } // Поддержка race detector (детектора гонок). if raceenabled { racereadrangepc(unsafe.Pointer(ptr), uintptr(n), sys.GetCallerPC(), abi.FuncPCABIInternal(slicebytetostring)) } // Поддержка MemorySanitizer. if msanenabled { msanread(unsafe.Pointer(ptr), uintptr(n)) } // Поддержка AddressSanitizer. if asanenabled { asanread(unsafe.Pointer(ptr), uintptr(n)) } if n == 1 { // Быстрая оптимизация для строк длины 1 — часто встречается. p := unsafe.Pointer(&staticuint64s[*ptr]) if goarch.BigEndian { // Смещение указателя в случае big-endian архитектуры. p = add(p, 7) } return unsafe.String((*byte)(p), 1) } var p unsafe.Pointer if buf != nil && n <= len(buf) { // Если доступен временный буфер, и он достаточно велик — используем его. p = unsafe.Pointer(buf) } else { // Иначе выделяем память в куче. p = mallocgc(uintptr(n), nil, false) } // Копируем байты в выделенную область. memmove(p, unsafe.Pointer(ptr), uintptr(n)) // Создаем строку из массива байт (без копирования). return unsafe.String((*byte)(p), n) } -
Преобразование из
stringв[]runeреализовано через функциюstringtoslicerune(внутриgo/src/runtime/string.go). Происходит декодирование UTF-8 →int32. Создаётся новый[]rune.func stringtoslicerune(buf *[tmpStringBufSize]rune, s string) []rune { n := 0 for range s { n++ } var a []rune if buf != nil && n <= len(buf) { *buf = [tmpStringBufSize]rune{} a = buf[:n] } else { a = rawruneslice(n) } n = 0 for _, r := range s { a[n] = r n++ } return a } -
Преобразование из
[]runeвstringреализовано через функциюslicerunetostring(внутриgo/src/runtime/string.go). Кодирует каждую руну обратно в UTF-8, результат — строка.func slicerunetostring(buf *tmpBuf, a []rune) string { if raceenabled && len(a) > 0 { racereadrangepc(unsafe.Pointer(&a[0]), uintptr(len(a))*unsafe.Sizeof(a[0]), sys.GetCallerPC(), abi.FuncPCABIInternal(slicerunetostring), ) } if msanenabled && len(a) > 0 { msanread(unsafe.Pointer(&a[0]), uintptr(len(a))*unsafe.Sizeof(a[0])) } if asanenabled && len(a) > 0 { asanread(unsafe.Pointer(&a[0]), uintptr(len(a))*unsafe.Sizeof(a[0])) } var dum [4]byte size1 := 0 for _, r := range a { size1 += encoderune(dum[:], r) } s, b := rawstringtmp(buf, size1+3) size2 := 0 for _, r := range a { if size2 >= size1 { break } size2 += encoderune(b[size2:], r) } return s[:size2] }
Получение длины массива байт
«The Go Programming Language»
Встроенная функция len возвращает количество байтов (не рун) в строке.
s := "hello, world" fmt.Println(len(s)) // "12"
В Go встроенная функция len() для строки реализована не как обычная функция, а как компиляторная операция: компилятор напрямую обращается к полю длины строки — без вызова отдельной функции.
ссылка на оригинал статьи https://habr.com/ru/articles/926732/
Добавить комментарий