Pull to refresh

Comments 21

Мне всегда казалось что стандартные реализации максимально производительные, но увы, не все так гладко.

Стандартные реализации в первую очередь универсальные — они должны работать везде и для всех. А это уже само по себе ограничивает возможность оптимизаций.

Иногда оптимизацией жертвуют ради читаемости/поддерживаемости кода
Правилом хорошего тона является в самом начале писать версию компилятора, которую вы тестируете.
Хорошее замечание. Сейчас добавлю.
Работа, безусловно, хорошая.

Но, теоретически, можно добиться гораздо большей производительности, если научить компилятор вообще избегать копирования данных (где это можно).
Например, если компилятор видит, что к строке идет много прибавлений — он может выделить сразу большой буффер и копировать только правую (после '+') часть. Это сейчас можно делать вручную через buf = append(buf, str)), но ручную работу лучше автоматизировать.
А ещё круче будет научить компилятор заранее указывать, куда писать результат, для функции, которая возвращает строку. Тогда можно будет обойтись вообще без копирования (если этот результат нужен для того, чтобы прибавить его к строке).
Эх, мечты, мечты… ))
Например, если компилятор видит, что к строке идет много прибавлений — он может выделить сразу большой буффер и копировать только правую (после '+') часть.

Для выражения x + y + z + ... буффер выделяется единожды. А вот как копировать только правую часть я не понял. У нас же должна новая строка в результате получиться, при этом ни один из операндов не должен изменяться.


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

Не уверен, что понимаю, как это может быть возможно.


Вообще + не так часто используется в самых горячих участках. Если конкатенация нетривиальная и нам нужно больше 2-3 сложений, то лучше использовать какой-нибудь bytes.Buffer или strings.Builder.


Эх, мечты, мечты… ))

Если что-то принципиально реализуемое и это не rocket science по сложности, то можно попробовать это привнести в компилятор. Первым шагом может быть написание proposal документа, если это проще, чем прототипирование сразу. Так вы сможете поделиться идеей/алгоритмом с людьми, которые будут в силах набросать прототип (если proposal воспримут положительно).

А вот как копировать только правую часть я не понял. У нас же должна новая строка в результате получиться, при этом ни один из операндов не должен изменяться.

например, выражение
buf += fmt.Sprintf(" %d", val)

либо
return(buf + fmt.Sprintf(" %d", val))

в обоих случаях тот buf что был до момента конкатенации как отдельная строка после конкатенации уже не нужен (а даже если бы и был ещё нужен — эту часть результирующей строки можно использовать как отдельную строку).

то лучше использовать какой-нибудь bytes.Buffer или strings.Builder.
Ну, есть способы, конечно (например strconv.AppendInt() вместо += strconv.Itoa(), но они заставляют программиста опускаться к деталям реализации, вместо того чтобы переложить эту работу по оптимизации на компилятор.

Не уверен, что понимаю, как это может быть возможно.
Когда мы используем
s3 = s1 + s2
то у нас выделяется память под s3 и туда копируется и s1, и s2. Хотя, если бы мы при выделении памяти под s1 зарезервировали в конце достаточно места, до было бы достаточно скопировать только s2 в конец s1, чтобы получить s3 (вот Вам и копирование только правой части — s1 не копируется, а остается в составе s3)

Или
s3 = s1 + S()
Если компилятор сможет сообщить S(), что результат надо возвращать не в отдельно выделенной памяти, а сохранить в конце s1 — тогда вообще не надо будет ничего копировать — S() сама запишет результат куда надо, останется только оформить новую строку.

Сразу говорю, что приятно видеть такие комментарии и мои ответы — это не придирки, а попытки полностью понять ваши предложения и, возможно, добавить контекста.


buf += fmt.Sprintf(" %d", val)

А если buf имеет алиасы? В Go строки не копируются при присваивании. Но вообще да, можно статически вывести безопасные случаи, когда buf гарантированно можно перезаписать.


s3 = s1 + S()

Интересно. Но это потребует разных специализаций S, потому что в общем случае нам нужно выделять новую строку, если, например, результат присваивается без конкатенации. Само тело функции для такого будет выделять новую память, а специализация для вашего случая потребует второго определения S, если я правильно понял, где не будет выделения памяти перед возвратом результата.


то у нас выделяется память под s3 и туда копируется и s1, и s2. Хотя, если бы мы при выделении памяти под s1 зарезервировали в конце достаточно места, до было бы достаточно скопировать только s2 в конец s1, чтобы получить s3

Это не беспроигрышный вариант, потому что подразумевает некоторый перерасход памяти. С другой стороны аллокатор всё равно обычно выделяет немного больше памяти, просто глядя на структуру stringStruct мы эту информацию получить не можем (нет поля cap).


Ну, есть способы, конечно (например strconv.AppendInt() вместо += strconv.Itoa(), но они заставляют программиста опускаться к деталям реализации, вместо того чтобы переложить эту работу по оптимизации на компилятор.

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

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

Это не беспроигрышный вариант, потому что подразумевает некоторый перерасход памяти.
Сейчас, чтобы получить результат конкатенации, все равно нужно выделять память под него, а потом придется освобождать старые буфера — тут и просто расход процессора, и фрагментация памяти…

Впрочем, анализ провести лишним не будет.
Он не будет репрезентативным. Горячие места оптимизированы через использование []byte, а строки используются там, где это делать лениво или муторно. С одной стороны, хорошо, что язык позволяет более-менее эффективно работать с последовательностями байт, но это требует от программиста усилий. А компактный наглядный код остается медленным, и никто это оптимизировать не спешит, ибо сложно, да и зачем, когда те, кому сильно надо, и так сделают через []byte.
> Кто то пишет в здравом уме на Го?

Почему бы и нет?

А к чему этот комментарий? Если это шутка, то не очень смешная, а если это всерьёз, то я не понимаю, откуда такая категоричность.
Отнюдь, очередной модный ЯП коих расплодилось десятки с дурацким маскотом.
Никто вас права собственного мнения не пытается лишить, но подавать его за единственное «здравое» — это немного перебор.

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

Но, если честно, это похоже на толстый троллинг.

(P.S. минусы вам не я поставил, но такую реакцию можно было ожидать, думаю, вы знали, на что идёте.)
Если нужна скорость, пишите на Си, коль у вашего Го с этим проблемы.
А почему не на Rust, например?
Почему именно C?

Статья не о том, что Go медленный, а о том, что операции над строками можно при желании ускорить. И в следующем релизе Go, возможно, будет одна или более оптимизаций, связанных с этим. А если эти оптимизации в Go не примут, то можно их будет как стороннюю библиотеку выложить, если кому-то действительно нужно как можно быстрее строки склеивать (хотя это довольно сомнительный случай).
Какой очаровательный тролль!!!

Не для вас далее комментарий, но для тех, кто может прочитать эту ветку.
Go — это про удачный компромисс. Мы поступаемся рядом фич, увеличивая объем кода, но уменьшая сложность программ и делая очень малое время компиляции. Мы поступаемся частью скорости и делаем достаточно простой по сути GC, но дающий предсказуемое время STW. И главное, что все это в целом дает очень легкое чтение и поддержание кода, минимальное время осноения новичков в проектах и большую скорость разработки в целом.
Golang — это про удачный компромисс. Если вам достаточно попадания в 95% требований по скорости от программ, то это может быть очень удачным выбором.
Если это шутка, то не очень смешная, а если это всерьёз, то я не понимаю, откуда такая категоричность.

Видимо тот самый случай, когда месяц писал проект на Go, а тимлид сказал, что нужно все переписать на C++.
Хотя уж, скорее, наоборот, но сути не меняет.

Привет. Вы писали:


Если посмотреть, какой код генерирует компилятор для оператора +, мы увидим вызовы функций concatstring2, concatstring3 и так далее (до concatstring5 включительно).

Как Вы узнали, какой код генерирует компилятор? Где это посмотреть?

Как Вы узнали, какой код генерирует компилятор? Где это посмотреть?

Способов несколько.
Самый простой — это передача компилятору ключа -gcflags=-S.


Для примера, соберите вот этот файл (назовём его foo.go):


package foo

func f1(x, y string) string { return x + y }
func f2(x, y, z string) string { return x + y + z }

С помощью команды go tool compile -S foo.go.
Помимо прочего, вы увидите вызовы runtime.concatstring2(SB) в f1 и runtime.concatstring3(SB) в f2.

Sign up to leave a comment.

Articles