Если вы были в сети и активно участвовали в сообществе программистов в течение последнего года, вы наверняка слышали похвалы за скорость и производительность выполнения Rust, а также за отличный тип Result в Rust.
Вероятно, мне следует упомянуть, что я не являюсь разработчиком Rust.
Несмотря на это или, может быть, даже из-за этого, мне интересно, как Rust может быть настолько производительным, если он использует этот тип Result, поскольку, насколько я понимаю, этот тип реализован как то, что можно было бы назвать union в C. Он включает в объединение ошибку и возвращаемое значение, из которых в данный момент времени допустимо только одно. Тип также содержит флаг, указывающий, содержит ли результат ошибку или значение.
Если я правильно посчитал и исходил из предположения, что ошибка хранится в виде указателя или ссылки (например, занимает 8 байт в памяти в 64-битных системах), то минимум 8 байт для объединения + один байт для флага составляют 9 байт памяти.
Что касается заполнения, я предполагаю, что в большинстве систем оно будет перераспределено и займет 12 байт. По сравнению с возвратом int(32) выделяется только 4 байта. Таким образом, использование Result должно выделить в три раза больше памяти, чем использование int.
Разве это не чрезмерная трата памяти? Я представляю, что если запустить это в цикле, это составит немало.
Я не совсем понимаю, как можно утверждать, что Rust очень производительен, в то время как Result занимает столько памяти?
Я знаю, что есть несколько приемов оптимизации, которые могут уменьшить использование памяти, например, использование NotZeroInt с опцией позволяет компилятору использовать ноль в качестве флага, тем самым избегая использования дополнительного байта для флага. Но для большинства типов это неприменимо, не так ли?
Если у кого-то есть дополнительная информация, я хотел бы это услышать. Имейте в виду, что я не являюсь разработчиком Rust и задаю этот вопрос из любопытства, поскольку я заметил в библиотеках, которые пытаются портировать эту функцию, резко увеличивается использование памяти.
Конечно, типы Result<T, E> и Option<T> в Rust оптимизированы лучше, чем некоторые портированные библиотеки, но я не могу себе представить, как это не влияет на производительность программы.
Почему вы «действуете исходя из предположения», когда можете проверить определение Результат и увидеть, что там нет указателя или ссылки
Кроме того, было бы полезно просмотреть примеры этих непроизводительных портов, чтобы в ответах можно было провести сравнение и показать, почему возникает разница в производительности.
Я думаю, что причина отрицательных голосов в том, что предположения в этом вопросе можно было бы легко доказать ложными, если бы вы просто проверили результат std::mem::size_of на нескольких типах Result, чтобы увидеть, насколько они велики. Обычно лучше доказать истинность или ложность своих предположений, прежде чем задавать вопрос, основанный на них.
На самом деле, да, я мог бы это сделать, но, как я уже упоминал, мне просто было любопытно, в моей системе нигде не установлена ржавчина..
@dragon, игровая площадка ржавчины (которую я использовал в одном из комментариев к ответу, поскольку на моем смартфоне нет ржавчины) — отличный ресурс, если вы хотите дальше поиграть, ничего не устанавливая.


В Rust гораздо более интеллектуально и оптимизировано распределение дискриминантов перечислений, чем в C с теговыми объединениями, потому что нет способа получить к ним прямой доступ. Кроме того, в Rust есть дженерики в качестве встроенной функциональности, а это означает, что макет Result<T, E> отдельно определяется и оптимизируется для каждой используемой комбинации T и E.
Благодаря дженерикам нет необходимости использовать ссылку: значения содержатся непосредственно в объединении перечисления результатов. Это означает, что минимум в большинстве случаев, когда ошибка представляет собой код ошибки, равен Error + Flag, который составляет 2 байта. (один для дискриминанта ошибки и один для дискриминанта флага).
Однако вам также необходимо иметь возможность возвращать значение, и это значение является обычным случаем. Это значение часто имеет дополнение и превышает значение ошибки. Если это так, компилятор Rust может использовать дополнение для дискриминатора для результата, в результате чего тегированное перечисление имеет тот же размер, что и его наибольшее значение. Это дает вам эффективное освобождение объектов Result с точки зрения использования памяти.
По большей части я согласен с двумя байтами, но однобайтовую ошибку, возможно, стоит показать ОП и другим читателям, насколько виды оптимизации возможны при правильных условиях.
Да, ок, логично, я предполагал, что будет ссылка на ошибку, которая явно не нужна.
Тогда как быть с сообщениями об ошибках?? Это ссылки???
это зависит от типа ошибки. Варианты включают предоставление функции, которая сопоставляет код ошибки с сообщением об ошибке (так же, как и с кодом ошибки C), или вы можете включить вариант перечисления, содержащий ссылку. Если вы это сделаете, то минимальный размер, скорее всего, составит не менее 24 байтов в 64-битной системе, но это имеет значение только в том случае, если ваш тип T также не имеет размер не менее 24 байтов, как это обычно бывает. Данные нужно будет как-то передать, и ржавчина дает вам ряд возможностей в зависимости от ситуации.
Кажется, вы знакомы с API-интерфейсами, которые возвращают целые числа для сообщения о статусе завершения: ноль в случае успеха и ненулевое значение в случае неудачи. API, выражающий то же самое в идиоматическом Rust, достигнет такой же или лучшей производительности1, но при этом будет более типобезопасным.
Первый шаг — использовать Result, чтобы классифицировать ценность как успех или неудачу. Поскольку в случае успеха нам не нужно возвращать ничего дополнительно, мы используем ():
Result<(), i32>
При этом он занимает больше места, чем простой i32, поскольку для передачи успеха необходим хотя бы один бит информации. Хотя, чтобы устранить одно из ваших заблуждений, для значения ошибки не существует косвенного указателя; он хранится в строке. Таким образом, это будет 4 байта для i32 + 1 байт для дискриминанта + 3 байта заполнения для достижения 4-байтового выравнивания = 8 байтов.
Как вы упомянули, можно использовать NonZeroI32:
Result<(), NonZeroI32>
В любом случае это могло бы быть потенциально более понятным (в противном случае получение Err(0) может сбить с толку, если вы привыкли к нулю, выражающему успех) и дает Result один дополнительный бит информации, так что размер теперь составляет всего 4 байта, поскольку компилятор может использовать все- нулевой битовый шаблон, который не используется NonZeroI32 для выражения успеха.
Однако это все еще не совсем идиоматический Rust, поскольку NonZeroI32 не особо усовершенствован. Чаще всего тип ошибки определяется как перечисление случаев ошибок:
enum MySimpleError {
FailureA,
FailureB,
FailureC,
}
Result<(), MySimpleError>
Теперь это гораздо более выразительно, поскольку теперь оно закодировано в типе, какие значения могут существовать, и даже меньше, поскольку MyError имеет гораздо меньше 256 вариантов, поэтому его можно закодировать как один байт с запасными битовыми шаблонами, так что целое Result представляет собой один байт.
1. It is not likely that values less than a machine word size actually improve performance - only by potentially reducing memory pressure if it can be stored with other small values.
Еще одна распространенная закономерность: иногда типы ошибок могут быть очень большими (попытка предоставить как можно больше полезной информации), но не являются распространенными или, по крайней мере, не должны быть распространенными. В этом случае вы можете ввести косвенность через Box для типа ошибки, чтобы уменьшить размер стека.
Result<(), Box<MyComplexError>>
Это будет размер машинного слова, поскольку Box гарантированно не будет нулевым, и, таким образом, в случае успеха можно использовать битовый шаблон со всеми нулями, как это возможно с NonZeroI32.
Вы можете обнаружить, что библиотеки уже могут делать это внутри себя. Например, Error из serde-json выглядит так:
/// This type represents all possible errors that can occur when serializing or
/// deserializing JSON data.
pub struct Error {
/// This `Box` allows us to keep the size of `Error` as small as possible. A
/// larger `Error` type was substantially slower due to all the functions
/// that pass around `Result<T, Error>`.
err: Box<ErrorImpl>,
}
Result<(), serde_json::Error> // does not use any extra space
Конечно, во многих случаях необходимо использовать дополнительное пространство для дискриминанта — либо из-за типа успеха, типа ошибки, либо из-за того и другого — но это просто необходимость выразить данные, которые вы хотите вернуть. Даже если требуется дополнительное машинное слово, обработка ошибок таким способом часто в целом более эффективна, чем затраты на выдачу исключений в случае возникновения ошибки.
E является универсальным, и исходный код стандартной библиотеки показывает, что он хранится внутри. Почему вы предполагаете, что он будет храниться за указателем или ссылкой?