Эффективное распределение ломтиков (ограничение по длине) - PullRequest
0 голосов
/ 07 июня 2019

Предполагается, что я создаю срез, о котором заранее знаю, что хочу заполнить цикл for элементами 1e5 через последовательные вызовы append:

// Append 1e5 strings to the slice

for i := 0; i<= 1e5; i++ {
    value := fmt.Sprintf("Entry: %d", i)
    myslice = append(myslice, value)
}

, который является более эффективным способом инициализации среза и почему:

а. объявлять ноль ломтик строк?

var myslice []string

б. установив его длину заранее 1e5?

myslice = make([]string, 1e5)

с. установка его длины и емкости на 1e5?

myslice = make([]string, 1e5, 1e5)

Ответы [ 2 ]

7 голосов
/ 07 июня 2019

Ваши решения b и c идентичны: при создании среза с make(), где вы не указываете емкость, «отсутствующая» емкость по умолчанию равна заданной длине.

Также обратите внимание, что если вы создаете срез с длиной заранее, вы не можете использовать append() для заполнения среза, потому что он добавляет новые элементы ксрез, и он не «повторно» использовать выделенные элементы.Таким образом, в этом случае вы должны присвоить значения элементам, используя индексное выражение , например, myslice[i] = value.

Если вы начинаете со среза с емкостью 0, новый резервный массив долженВы должны быть выделены, а «старый» контент должен копироваться всякий раз, когда вы добавляете элемент, который не вписывается в емкость, поэтому решение должно быть медленнее по своей природе.

Я бы определил и рассмотрел следующие различные решения (Iиспользуйте срез []int, чтобы fmt.Sprintf() не вмешивался в наши тесты и не мешал им):

var s []int

func BenchmarkA(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s = nil
        for j := 0; j < 1e5; j++ {
            s = append(s, j)
        }
    }
}

func BenchmarkB(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s = make([]int, 0, 1e5)
        for j := 0; j < 1e5; j++ {
            s = append(s, j)
        }
    }
}

func BenchmarkBLocal(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := make([]int, 0, 1e5)
        for j := 0; j < 1e5; j++ {
            s = append(s, j)
        }
    }
}

func BenchmarkD(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s = make([]int, 1e5)
        for j := range s {
            s[j] = j
        }
    }
}

Примечание. Я использую переменные уровня пакета в тестах производительности (кроме BLocal), поскольку некоторыеОптимизация может (и в действительности происходит) при использовании локальной переменной слайса).

И результаты теста:

BenchmarkA-4         1000     1081599 ns/op     4654332 B/op     30 allocs/op
BenchmarkB-4         3000      371096 ns/op      802816 B/op      1 allocs/op
BenchmarkBLocal-4   10000      172427 ns/op      802816 B/op      1 allocs/op
BenchmarkD-4        10000      167305 ns/op      802816 B/op      1 allocs/op

A: Как вы можете видеть, начиная сnil слайс является самым медленным, использует больше всего памяти и выделений.

B: Предварительное выделение слайса с емкостью (но все равно 0 длиной) и использованием добавления: для этого требуется только не замужемвыделение и намного быстрее, почти в три раза быстрее .

BLocal: обратите внимание, что при использовании локального слайса вместо переменной пакета (оптимизация компилятора) происходят оптимизации ион становится намного быстрее: в два раза быстрее, почти так же быстро, как D.

D: если не использовать append(), но присвоение элементов предварительно выделенному слайсу выигрывает во всех аспектахдаже при использовании нелокальной переменной.

0 голосов
/ 07 июня 2019

Для этого варианта использования, поскольку вы уже знаете количество строковых элементов, которые вы хотите назначить срезу,

Я бы предпочел подход b или c .

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

Если вы решите использовать подход a , срез будет удваивать свой размер каждый раз, когда добавляется новый элемент после того, как len равняется емкости.

https://play.golang.org/p/kSuX7cE176j

...