Слайсы изнутри: память, capacity и подводные камни

Слайсы изнутри: память, capacity и подводные камни

В уроке по слайсам мы научились использовать слайсы. Теперь разберём, как они работают под капотом.

Структура слайса

Слайс - это структура из трёх полей:

// Примерно так выглядит внутри runtime
type slice struct {
    array unsafe.Pointer // указатель на массив
    len   int            // длина
    cap   int            // ёмкость
}
s := make([]int, 3, 5)
// array → [0, 0, 0, _, _]
// len = 3
// cap = 5
<?php
declare(strict_types=1);

// PHP-массив - это упорядоченная hash-table (HashTable struct в Zend)
// Нет cap(), нет ptr - всё под капотом
$s = [0, 0, 0];
// count($s) === 3 - это всё, что доступно наружу

В PHP массив - это упорядоченная хеш-таблица «из коробки», а не linear memory. Реальных slice header (ptr/len/cap) или ручных «buckets» в PHP нет - Zend engine прячет это внутри. PHP-пример показывает API-эквивалент, а не точное воспроизведение Go-памяти.

Slice header в Go: указатель, len, cap и backing array

Как работает append

s := make([]int, 0, 4)
// cap=4, массив выделен

s = append(s, 1, 2, 3) // len=3, cap=4 - влезает
s = append(s, 4)        // len=4, cap=4 - последний слот
s = append(s, 5)        // len=5, cap=8 - новый массив!

Когда len == cap, Go выделяет новый массив (обычно x2) и копирует данные.

<?php
declare(strict_types=1);

// PHP-эквивалент append: оператор []= или array_push()
$s = [];
$s[] = 1;
$s[] = 2;
$s[] = 3;
array_push($s, 4, 5); // эквивалентно нескольким []=

// PHP-массив - value type. По умолчанию передаётся копией:
function modifyByValue(array $s): array
{
    $s[] = 999;
    return $s;
}

// Если нужно изменить in-place - явный reference:
function modifyByRef(array &$s): void
{
    $s[] = 999;
}

Когда buffer переполняется - Zend перевыделяет память внутри HashTable, но capacity не выставляется наружу. На бенчмарках это всё ещё amortized O(1).

Подслайс ссылается на тот же массив. Изменения в одном видны в другом - пока не произойдёт расширение.
original := []int{1, 2, 3, 4, 5}
sub := original[1:3] // [2, 3], но cap=4!

sub = append(sub, 99)
// sub = [2, 3, 99]
// original = [1, 2, 3, 99, 5] - СЮРПРИЗ!
<?php
declare(strict_types=1);

$original = [1, 2, 3, 4, 5];
$sub = array_slice($original, 1, 2); // [2, 3] - независимая копия

$sub[] = 99;
// $sub === [2, 3, 99]
// $original === [1, 2, 3, 4, 5] - никаких сюрпризов

В PHP такой ловушки нет: array_slice($original, 1, 2) всегда возвращает новый массив. Backing array не разделяется - Zend сразу копирует. Минус: чуть больше памяти и аллокаций; плюс: не выстрелишь в ногу.

Безопасное копирование

// Полная копия - безопасно
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)

// Или через append
dst2 := append([]int(nil), src...)
<?php
declare(strict_types=1);

// Простое присваивание уже копирует (copy-on-write до первой модификации)
$src = [1, 2, 3];
$dst = $src;
$dst[] = 99;
// $src === [1, 2, 3]

// Spread в литерал
$dst2 = [...$src];

// Или array_merge
$dst3 = array_merge([], $src);

Предаллокация

// ПЛОХО: постоянные реаллокации
var result []int
for i := 0; i < 10000; i++ {
    result = append(result, i)
}

// ХОРОШО: одна аллокация
result := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    result = append(result, i)
}
<?php
declare(strict_types=1);

// В PHP «явной» предаллокации нет: $arr = []; $arr[] = ... работает,
// но Zend сам ресайзит HashTable пошагово (степени двойки).
// Если очень нужна гарантия - SplFixedArray с known size:
$result = new \SplFixedArray(10000);
for ($i = 0; $i < 10000; $i++) {
    $result[$i] = $i;
}
// SplFixedArray - C-array, без hash overhead. Быстрее на больших размерах
// но не поддерживает строковые ключи и динамический рост.

\SplFixedArray - редкий случай, когда PHP даёт честную «фиксированную ёмкость». Внутри это C-массив указателей, без HashTable.

Стратегия роста

До Go 1.18: удваивание до 1024, затем рост на 25%. С Go 1.18: более плавная формула, но суть та же - amortized O(1).

func main() {
    var s []int
    prevCap := 0
    for i := 0; i < 20; i++ {
        s = append(s, i)
        if cap(s) != prevCap {
            fmt.Printf("len=%2d cap=%2d\n", len(s), cap(s))
            prevCap = cap(s)
        }
    }
}
// len= 1 cap= 1
// len= 2 cap= 2
// len= 3 cap= 4
// len= 5 cap= 8
// len= 9 cap=16
// len=17 cap=32
<?php
declare(strict_types=1);

// В PHP capacity не видна снаружи - Zend сам ресайзит HashTable степенями двойки.
// Косвенно можно наблюдать через memory_get_usage():
$s = [];
$prevMem = 0;
for ($i = 0; $i < 20; $i++) {
    $s[] = $i;
    $mem = memory_get_usage();
    if ($mem !== $prevMem) {
        echo 'len=' . count($s) . ' mem_delta=' . ($mem - $prevMem) . "\n";
        $prevMem = $mem;
    }
}
// Скачки memory_get_usage показывают реаллокации - тот же smооth amortized O(1)

Рост capacity слайса при append: realloc на каждом удвоении

Утечки памяти через слайсы

// ПЛОХО: держим весь массив в памяти
func getHeader(data []byte) []byte {
    return data[:16] // весь data не соберётся GC!
}

// ХОРОШО: копируем нужное
func getHeader(data []byte) []byte {
    header := make([]byte, 16)
    copy(header, data[:16])
    return header
}
<?php
declare(strict_types=1);

function getHeader(string $data): string
{
    // substr возвращает новую строку, исходная $data может быть собрана GC
    return substr($data, 0, 16);
}

Этой ловушки в PHP нет: substr($data, 0, 16) или array_slice() всегда копируют, не удерживая ссылку на исходник. PHP не разделяет backing arrays между объектами на уровне языка.

<?php
declare(strict_types=1);

// PHP-аналог removeDuplicates in-place для отсортированного array.
// PHP-array - hash, но мы можем переписать его «в две позиции»:
function removeDuplicates(array &$s): int
{
    $n = count($s);
    if ($n === 0) {
        return 0;
    }
    $write = 1;
    for ($read = 1; $read < $n; $read++) {
        if ($s[$read] !== $s[$write - 1]) {
            $s[$write] = $s[$read];
            $write++;
        }
    }
    // Обрезаем хвост:
    $s = array_slice($s, 0, $write);
    return $write;
}

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