Go: несколько вызовов len() против производительности?

На данный момент я реализую некоторые алгоритмы сортировки. Поскольку это характерно для алгоритмов, существует множество вызовов длины некоторых массивов/срезов с использованием метода len().

Теперь, учитывая следующий код для (части) алгоритма Mergesort:

  for len(left) > 0 || len(right) > 0 {
        if len(left) > 0 && len(right) > 0 {
            if left[0] <= right[0] {
                result = append(result, left[0])
                left = left[1:len(left)]
            } else {
                result = append(result, right[0])
                right = right[1:len(right)]
            }
        } else if len(left) > 0 {
            result = append(result, left[0])
            left = left[1:len(left)]
        } else if len(right) > 0 {
            result = append(result, right[0])
            right = right[1:len(right)]
        }
    }

Мой вопрос: влияют ли эти множественные вызовы len() отрицательно на производительность алгоритма? Не лучше ли сделать временную переменную на длину среза right и left? Или компилятор сам это делает?


person Andreas Fröwis    schedule 29.10.2014    source источник
comment
если есть сомнения, эталон и/или профиль.   -  person JimB    schedule 29.10.2014
comment
Это странный вопрос для кода, который выполняет распределения. Что бы вы ответили, если бы кто-то спросил: «Повышает ли подача соленого масла на авианосце стоимость миссий?». Да, но это не имеет значения (при условии, что соленое масло дороже).   -  person Volker    schedule 29.10.2014


Ответы (2)


Есть два случая:

  • Локальный срез: длина будет кэшироваться без накладных расходов.
  • Глобальный срез или переданный (по ссылке): длина не может быть кэширована, и есть накладные расходы

Нет накладных расходов на локальные срезы

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

func generateSlice(x int) []int {
    return make([]int, x)
}

func main() {
    x := generateSlice(10)
    println(len(x))
}

В компиляции с go tool 6g -S test.go это дает, среди прочего, следующие строки:

MOVQ    "".x+40(SP),BX
MOVQ    BX,(SP)
// ...
CALL    ,runtime.printint(SB)

Здесь происходит то, что первая строка извлекает длину x, получая значение, расположенное в 40 байтах от начала x, и, что наиболее важно, кэширует это значение в BX, которое затем используется для каждого вхождения len(x). Причина смещения в том, что массив имеет следующую структуру (источник):

typedef struct
{               // must not move anything
    uchar   array[8];   // pointer to data
    uchar   nel[4];     // number of elements
    uchar   cap[4];     // allocated number of elements
} Array;

nel — это то, к чему обращается len(). Вы можете увидеть это в генерация кода.

Глобальные и ссылочные срезы имеют накладные расходы

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

func accessLocal() int {
    a := make([]int, 1000) // local
    count := 0
    for i := 0; i < len(a); i++ {
        count += len(a)
    }
    return count
}

var ag = make([]int, 1000) // pseudo-code

func accessGlobal() int {
    count := 0
    for i := 0; i < len(ag); i++ {
        count += len(ag)
    }
    return count
}

Сравнение сборки обеих функций дает ключевую разницу в том, что, как только переменная становится глобальной, доступ к атрибуту nel больше не кэшируется, и будут накладные расходы времени выполнения:

// accessLocal
MOVQ    "".a+8048(SP),SI // cache length in SI
// ...
CMPQ    SI,AX            // i < len(a)
// ...
MOVQ    SI,BX
ADDQ    CX,BX
MOVQ    BX,CX            // count += len(a)

// accessGlobal
MOVQ    "".ag+8(SB),BX
CMPQ    BX,AX            // i < len(ag)
// ...
MOVQ    "".ag+8(SB),BX
ADDQ    CX,BX
MOVQ    BX,CX            // count += len(ag)
person nemo    schedule 29.10.2014
comment
отличный ответ ... и супер круто, что компилятор Go делает это под капотом. - person Ralph Caraveo; 30.10.2014
comment
В функции нет синхронизации, поэтому модель памяти go позволяет компилятору кэшировать len(), откуда бы ни был получен фрагмент. Из анализа в вашем ответе видно, что это не оптимизация, которую gc делает в настоящее время, но может быть в будущем. - person Paul Hankin; 30.10.2014
comment
@ Анонимный AFAICS да. Но даже если процесс синхронизирован, компилятору потребуется добавить код для обновления кэшированного значения после того, как вы передадите срез другой горутине. Компилятор должен знать, где происходит синхронизация, и вставить код для обновления длины ниже этой. - person nemo; 30.10.2014
comment
Привет, в вашей функции accessLocal вы используете глобальную переменную ag и локальную переменную лота a - person user3219492; 07.08.2017

Несмотря на хорошие ответы, которые вы получаете, я получаю более низкую производительность, если постоянно вызываю len(a), например, в этом тесте http://play.golang.org/p/fiP1Sy2Hfk

package main

import "testing"

func BenchmarkTest1(b *testing.B) {
    a := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        count := 0
        for i := 0; i < len(a); i++ {
            count += len(a)
        }
    }
}

func BenchmarkTest2(b *testing.B) {
    a := make([]int, 1000)
    for i := 0; i < b.N; i++ {
        count := 0
        lena := len(a)
        for i := 0; i < lena; i++ {
            count += lena
        }
    }
}

При запуске как go test -bench=. я получаю:

BenchmarkTest1   5000000               668 ns/op
BenchmarkTest2   5000000               402 ns/op

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

person siritinga    schedule 29.10.2014
comment
Для корректного сравнения срез также должен быть локальной переменной. - person ; 29.10.2014
comment
Он попытался с локальной переменной, и результаты были такими же. И если count объявлен локально, разница важна, но я не знаю, почему. - person siritinga; 29.10.2014
comment
Общие фрагменты @siritinga, такие как глобальные, обрабатываются по-разному и имеют накладные расходы во время выполнения. Я обновил свой ответ, чтобы охватить этот случай. - person nemo; 30.10.2014
comment
@nemo Спасибо за ваш ответ. Я изменил код, теперь count и slice локальны, а разница осталась. Я не умею читать ассемблер, поэтому не знаю почему, но разница есть. - person siritinga; 30.10.2014
comment
@siritinga Используйте b.ResetTimer() после создания фрагмента. Здесь вы измеряете время создания среза. Второй тест, вероятно, повторно использует пространство первого среза. - person nemo; 30.10.2014
comment
@nemo, использующий ResetTimer(), не меняет время. Это ожидаемо, потому что оно выделяется только один раз на тест, поэтому им можно пренебречь. И если я поменяю порядок, я получу тот же результат. Значит, происходит что-то еще. - person siritinga; 31.10.2014
comment
из-за ответа @nemo с большим количеством голосов, тест для глобального массива был бы очень полезен - person Grijesh Chauhan; 27.08.2018