Спойлер: C++ не быстрее и не медленнее и вообще смысл не в этом. Эта статья является продолжением славных традиций развенчания мифов крупных российских компаний о языке Rust. Предыдущая была "Go быстрее Rust, Mail.Ru Group сделала замеры".
Недавно я пытался заманить коллегу, сишника из соседнего отдела, на Тёмную сторону Rust. Но мой разговор с коллегой не задался. Потому что, цитата:
В 2019 году я был на конференции C++ CoreHard, слушал доклад Антона antoshkka Полухина о незаменимом C++. По словам Антона, Rust еще молодой, не очень быстрый и вообще не такой безопасный.
Антон Полухин является представителем России в ISO на международных заседаниях рабочей группы по стандартизации C++, автором нескольких принятых предложений к стандарту языка C++. Антон действительно крутой и авторитетный человек в вопросах по C++. Но доклад содержит несколько серьёзных фактических ошибок в отношении Rust. Давайте их разберём.
Речь идет об этом докладе с 13:00 по 22:35.
Оглавление
- Миф №1. Арифметика в Rust ничуть не безопасней C++
- Миф №2. Плюсы Rust только в анализе времени жизни объектов
- Миф №3. Вызовы функций в Rust бездумно трогают память
- Миф №4. Rust медленнее C++
- Миф №5. C > С++ — noop, C > Rust — PAIN!!!!!!!
- Миф №6. unsafe отключает все проверки Rust
- Миф №7. Rust не поможет с сишными библиотеками
- Миф №8. Безопасность Rust не доказана
- Заключение
Миф №1. Арифметика в Rust ничуть не безопасней C++.
Для примера сравнения ассемблерного выхлопа Антон взял функцию возведения в квадрат(link:godbolt):
Rust | C++ |
---|---|
|
|
|
|
Цитата (13:35):
Получаем одинаковый ассемблерный выхлоп. Отлично! У нас есть базовая линия. Пока что C++ и Rust выдает одно и то же.
В самом деле, ассемблерный листинг арифметического умножения в обоих случаях выглядит одинаковым, но это только до поры до времени. Дело в том, что с точки зрения семантики языков, код делает разные вещи. Этот код определяет функции возведения числа в квадрат, но в случае Rust область определения [-2147483648, 2147483647], а в случае C++ это [-46340, 46340]. Как такое может быть? Магия?
Магические константы -46340 и 46340 — это максимальные по модулю аргументы, квадрат которых умещается в std::int32_t
. Все что выше будет давать неопределенное поведение из-за signed overflow. Если не верите мне, послушайте PVS-Studio. И если вы достаточно удачливы, чтобы работать в командах, которые настроили себе CI с проверкой кода на определенное поведение, вы получите такое сообщение:
runtime error: signed integer overflow: 46341 * 46341 cannot be represented in type 'int'
runtime error: signed integer overflow: -46341 * -46341 cannot be represented in type 'int'
В Rust такая ситуация с неопределенным поведением в арифметике невозможна в принципе.
Давайте послушаем, что об этом думает Антон (13:58):
Неопределенное поведение заключается в том что у нас тут знаковое число, и компилятор C++ считает что переполнения знаковых чисел не должно происходить в программе. Это неопределенное поведение. За счет этого компилятор C++ делает множество хитрых оптимизаций. В компиляторе Rust'а это задокументированное поведение, но от этого вам легче не станет. Ассемблерный код у вас получается тот же самый. В Rust'е это задокументированное поведение, и при умножении двух больших положительных чисел вы получите отрицательное число, что скорее всего не то, что вы ожидали. При этом за счет того, что они документируют это поведение, Rust теряет возможность делать многие оптимизации. Они у них прям где-то на сайте написаны.
Я бы почитал, какие оптимизации не умеет Rust, особенно с учётом того, что в основе Rust лежит LLVM — тот же самый бэкенд, что и у Clang. Соответственно, Rust «бесплатно» получил и разделяет с C++ большую часть независящих от языка трансформаций кода и оптимизаций. И хотя в представленном примере мы и получили одинаковый ассемблер, на самом деле, это случайность. Хитрые оптимизации и наличие неопределённого поведения при переполнении знакового в языке C++ могут приводить к веселью и порой порождают такие статьи. Рассмотрим эту статью подробнее.
Дан код функции, вычисляющей полиномиальный хеш от строки с переполнением int'a:
unsigned MAX_INT = 2147483647;
int hash_code(std::string x) {
int h = 13;
for (unsigned i = 0; i < 3; i++) {
h += h * 27752 + x[i];
}
if (h < 0) h += MAX_INT;
return h;
}
На некоторых строках, в частности, на строке «bye», и только на сервере (что интересно, на своем компьютере все было в порядке) функция возвращала отрицательное число. Но как же так, ведь в случае, если число отрицательное, к нему прибавится MAX_INT и оно должно стать положительным.
Как подсказывает PVS-Studio, неопределенное поведение действительно не определено. Если посчитать 27752 в 3 степени, можно понять, почему хэш от двух букв считается нормально, а от трех уже с какими-то странными результатами.
Аналогичный код на Rust будет вести себя корректно(link:playground):
fn hash_code(x: String) -> i32 {
let mut h = 13i32;
for i in 0..3 {
h += h * 27752 + x.as_bytes()[i] as i32;
}
if h < 0 {
h += i32::max_value();
}
return h;
}
fn main() {
let h = hash_code("bye".to_string());
println!("hash: {}", h);
}
Выполнение этого кода отличается в Debug и Release по понятным причинам, а для унификации поведения можно воспользоваться семейством функций: wrapping*, saturating*, overflowing* и checked*.
Как видите, документированное поведение и отсутствие неопределённого поведения при переполнении знакового действительно делают жизнь легче.
Вычисление квадрата числа — это отличный пример того, как можно выстрелить себе в ногу с помощью C++ в трех строчках кода. Зато быстро и с оптимизациями. Если от обращения к неинициализированной памяти ещё можно откреститься вдумчивым взглядом, то проблема с арифметикой в том, что беда может прийти совершенно внезапно и на «голом» арифметическом коде, где ломаться на первый взгляд нечему.
Миф №2. Плюсы Rust только в анализе времени жизни объектов.
В качестве примера приводится следующий код(link:godbolt):
Rust | C++ |
---|---|
|
|
|
|
Антон (15:15):
Компилятор Rust'а и компилятор C++ скомпилировали оба этих приложения, и функция bar
ничего не делает. При этом оба компилятора выдали сообщения-предупреждения, что возможно здесь что-то не то. К чему я все это говорю… Когда вы слышите, что Rust супер замечательный безопасный язык, то его безопасность заключается только в анализе времени жизни объектов, UB — либо документированное поведение, которое вы не очень ожидаете, по-прежнему в нем есть. Компилятор по-прежнему компилирует код, который явно делает какую-то чушь. И-и-и так уж получается.
Здесь мы наблюдаем бесконечную рекурсию. Опять-таки код компилируется в одинаковый ассемблерный выхлоп, то есть NOP для функции bar
как в C++, так и в Rust. Но это баг LLVM.
Если вывести LLVM IR кода с бесконечной рекурсией, то мы увидим(link:godbolt):
code |
|
asm |
|
IR |
|
ret i32 undef
— и есть ошибка, сгенерированная LLVM.
В самом LLVM бага живет с 2006 года. И это важный вопрос, ведь необходимо иметь возможность пометить бесконечные цикл или рекурсию так, чтобы LLVM не мог оптимизировать это в ноль. К счастью, есть прогресс. В LLVM 6 добавили интринсик llvm.sideeffect, а в 2019 году в rustc был добавлен флаг -Z insert-sideeffect
, который добавляет llvm.sideeffect
в бесконечные циклы и рекурсии. И бесконечная рекурсия становится действительно бесконечной(link:godbolt). Надеюсь, что в скором времени этот флаг перейдет и в stable rustc по умолчанию.
В C++ бесконечная рекурсия и цикл без побочных эффектов считаются неопределённым поведением, так что от этой баги LLVM страдают только Rust и C.
Итак, после того, как мы разобрались с ошибкой LLVM, давайте перейдем к главному заявлению: "его безопасность заключается только в анализе времени жизни объектов". Это заявление ложно, так как безопасное подмножество Rust защищает от ошибок, связанных с многопоточностью, гонками данных и выстрелами по памяти.
Миф №3. Вызовы функций в Rust бездумно трогают память.
Антон (16:00):
Посмотрим более сложные функции. Что с ними делает Rust. Поправили нашу функциюbar
и теперь она вызывает функциюfoo
. Мы видим, что Rust сгенерировал две лишних инструкции: одна инструкция сохраняет что-то в стек, другая инструкция в конце вытаскивает со стека. В C++ этого нету. Rust два раза потрогал память. Как-то уже не очень.
Вот этот пример(link:godbolt):
Rust | C++ |
---|---|
|
|
|
|
Вывод ассемблера для Rust длинный, но мы разберемся в причинах такой разницы. В этом примере Антон использует флаги -ftrapv
для C++ и -C overflow-checks=on
для Rust, чтобы включить проверку на переполнение знаковых. При переполнении C++ прыгает на инструкцию ud2
, которая приводит к "Illegal instruction (core dumped)", а Rust прыгает на вызов функции core::panicking::panic
, подготовка к которой занимает половину ассемблерного кода. В случае переполнения core::panicking::panic
дает нам красивое объяснение падения:
$ ./signed_overflow
thread 'main' panicked at 'attempt to multiply with overflow', signed_overflow.rs:6:12
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Так откуда взялись эти "лишние" инструкции, которые трогают память? Соглашение о вызове функции x86-64 требует, чтобы стек был выравнен до 16 байт, инструкция call
кладёт 8-байтовый адрес возврата на стек, что ломает выравнивание. Чтобы это исправить, компиляторы кладут всякие инструкции типа push rax. И так делает не только Rust, но и C++(link:godbolt):
Rust | C++ |
---|---|
|
|
|
|
И C++, и Rust сгенерировали одинаковый выхлоп ассемблера, оба добавили push rbx
для выравнивания стека. Q.E.D.
Самое интересное заключается в том, что именно C++ нуждается в деоптимизации кода путём добавления аргумента -ftrapv
, чтобы ловить неопределенное поведение при переполнении знаковых. Выше я уже показал, что Rust будет вести себя корректно даже без флага -C overflow-checks=on
, так что можете сравнить сами(link:godbolt) стоимость корректного кода на C++, либо почитайте статью на эту тему. К тому же -ftrapv
в gcc сломан с 2008 года.
Миф №4. Rust медленнее C++.
Антон (18:10):
Чуть медленнее Rust плюсов...
На протяжении всего доклада Антон выбирает примеры, написанные на Rust'е, которые компилируются в чуть больший ассемблер. Не только примеры выше, которые "трогают" память, но и пример на 17:30(link:godbolt):
Rust | C++ |
---|---|
|
|
|
|
Складывается впечатление, что весь этот анализ ассемблерного выхлопа был нужен лишь для того, чтобы сказать, что больше асма > медленнее язык.
В 2019 на конференции CppCon был интересный доклад There Are No Zero-cost Abstractions от Chandler Carruth. Вот он там на 17:30 сильно страдает из-за того, что std::unique_ptr
стоит дороже сырых указателей (link:godbolt). И чтобы хоть как-то приблизиться к ассемблерному выхлопу кода на сырых указателях ему приходится добавлять noexcept
, rvalue ссылки, и использовать std::move
. А на Rust всё будет работать без дополнительных усилий. Давайте сравним два кода и ассемблер. В примере на Rust мне пришлось дополнительно извратиться с extern "Rust"
и unsafe
, чтобы компилятор не заинлайнил вызовы (link:godbolt):
Rust | C++ |
---|---|
|
|
|
|
При меньших трудозатратах Rust генерирует меньше ассемблера. И не нужны подсказки компилятору в виде noexcept
, rvalue ссылок и std::move
. В сравнениях языков нужны нормальные бенчмарки. Нельзя вытащить понравившийся пример, и утвержать, что один язык медленнее другого.
В декабре 2019 Rust превосходил по производительности C++ согласно результатам Benchmarks Game. С тех пор C++ немного укрепил свои позиции. Но на таких синтетических бенчмарках языки будут раз за разом обходить друг друга. Я бы не отказался посмотреть нормальные бенчмарки.
Миф №5. C > С++ — noop, C > Rust — PAIN!!!!!!!
Антон (18:30):
Мы берем большое десктопное плюсовое приложение, пытаемся его переписать на Rust и понимаем, что наше большое плюсовое приложение использует сторонние библиотеки. А очень много сторонних библиотек, написанных на си, имеют сишные заголовочные файлы. Из С++ эти заголовочные файлы мы можем брать и использовать, по возможности оборачивая все в более безопасные конструкции. В Rust'е нам придется переписать эти заголовочные файлы либо сгенерировать какой-то программой из сишных заголовочных файлов.
Вот тут Антон смешал в одну кучу объявление сишных функций и их последующее использование.
Действительно, объявление сишных функций в Rust требует либо их ручного объявления, либо автоматической генерации, потому что это разные языки программирования. Подробнее можно прочитать в моей статье про бота для Starcraft, либо посмотреть на пример генерации этих оберток.
К счастью, у языка Rust есть пакетный менеджер cargo, который позволяет один раз сгенерировать объявления и поделиться ими со всем миром. Как вы понимаете, люди делятся не только сырыми объявлениями, но и безопасными и идиоматичными обёртками. На 2020 год в реестре пакетов crates.io находится около 40 000 крейтов.
Ну а само использование сишной библиотеки занимает буквально одну строчку в вашем конфиге:
# Cargo.toml
[dependencies]
flate2 = "1.0"
Всю работу по компиляции и линковке с учетом версий зависимостей cargo выполнит автоматически. Пример с flate2 примечателен тем, что в начале своего существования этот крейт использовал сишную библиотеку miniz, написанную на C, но со временем сообщество переписало сишный код на Rust. И flate2 стал работать быстрее.
Миф №6. unsafe отключает все проверки Rust.
Антон (19:14):
Внутри блока unsafe
отключаются все проверки Rust'а, он там ничего не проверяет, и целиком полагается на то, что вы в этом месте написали все правильно.
Данный пункт является продолжением темы про интеграцию сишных библиотек в Rust'овый код.
Увы, мнение об отключении всех проверок в unsafe
— это типичное заблуждение, потому что в документации к языку Rust сказано, что unsafe
позволяет:
- Разыменовывать сырой указатель;
- Вызывать и объявлять unsafe функции;
- Читать или измененять статическую изменяемую переменную;
- Реализовывать и объявлять unsafe типаж;
- Получать доступ к полям
union
.
Ни о каких отключениях всех проверок Rust здесь и речи не идет. Если у вас ошибка с lifetime-ами, то просто добавление unsafe
не поможет коду скомпилироваться. Внутри этого блока компилятор продолжает проверять код на соответствие системы типов, отслеживать время жизни переменных, корректность на потокобезопасность и многое-многое другое. Подробнее можно прочитать в статье You can’t "turn off the borrow checker" in Rust.
К unsafe
не стоит относиться как "я делаю, что хочу". Это указание компилятору, что вы берете на себя ответственность за вполне конкретный набор инвариантов, которые компилятор самостоятельно проверить не может. Например, разыменование сырого указателя. Это мы с вами знаем, что сишный malloc
возвращает NULL или указатель на аллоцированный кусок неинициализированной памяти, а компилятор Rust об этой семантике ничего не знает. Поэтому для работы с сырым указателем, который вернул, к примеру, malloc
, вы должны сказать компилятору: "я знаю, что делаю; я проверил, там не нулл, память правильно выравнена для этого типа данных". Вы берете на себя ответственность за этот указатель в блоке unsafe
.
Миф №7. Rust не поможет с сишными библиотеками.
Антон (19:25):
Из десяти ошибок за последний месяц которые я встречал и которые возникали в C++ программах три были вызваны тем, что с сишным методом неправильно работают, где-то забыли освободить память где-то не тот аргумент передали, где-то не проверили на null и передали нулевой указатель. Огромное количество проблем именно в использовании сишного кода. И в этом месте Rust вам никак не поможет. Получается как-то не очень хорошо. Вроде бы у Rust с безопасностью намного лучше, но только мы начинаем использовать сторонние библиотеки, нужно иметь такую же бдительность как в C++.
По статистике Microsoft, 70% уязвимостей связаны с нарушениями безопасности доступа к памяти и с другими классами ошибок, которые Rust предотвращает ещё на этапе компиляции. Это ошибки, которые физически невозможно совершить в безопасном подмножестве Rust.
С другой стороны, существует и unsafe
подмножество Rust, которое позволяет разыменовывать сырые указатели, вызывать сишные функции… и прочие небезопасные вещи, которые могут сломать вашу программу, если ими пользоваться неправильно. В общем, именно то, что делает Rust системным языком программирования.
И, казалось бы, можно поймать себя на мысли, что если в Rust и в C++ надо следить за корректностью вызовов сишных функций, то Rust ничуть не выигрывает. Но особенностью Rust является возможность разграничения кода на безопасный и потенциально опасный с последующей инкапсуляцией последнего. А если на текущем уровне гарантировать корректность семантики не удаётся, то unsafe
надо делегировать вызывающему коду.
На практике делегация unsafe
наверх выглядит вот так:
// Warning: Calling this method with an out-of-bounds index is undefined behavior.
unsafe fn unchecked_get_elem_by_index(elems: &[u8], index: usize) -> u8 {
*elems.get_unchecked(index)
}
slice::get_unchecked
— это стандартная unsafe
функция, которая получает элемент по индексу без проверок индекса на выход за границы. Так как в нашей функции get_elem_by_index
мы тоже не проверяем индекс, а передаем его как есть, то наша функция потенциально опасна. И любое обращение к такой функции требует явного указания unsafe
(link:playground):
// Warning: Calling this method with an out-of-bounds index is undefined behavior.
unsafe fn unchecked_get_elem_by_index(elems: &[u8], index: usize) -> u8 {
*elems.get_unchecked(index)
}
fn main() {
let elems = &[42];
let elem = unsafe { unchecked_get_elem_by_index(elems, 0) };
dbg!(elem);
}
Если вы передадите индекс, выходящий за границы, то получите обращение к неинициализированной области памяти. И сделать вы это сможете только в unsafe
.
Тем не менее, с помощью этой unsafe
функции мы можем построить безопасную версию(link:playground):
// Warning: Calling this method with an out-of-bounds index is undefined behavior.
unsafe fn unchecked_get_elem_by_index(elems: &[u8], index: usize) -> u8 {
*elems.get_unchecked(index)
}
fn get_elem_by_index(elems: &[u8], index: usize) -> Option<u8> {
if index < elems.len() {
let elem = unsafe { unchecked_get_elem_by_index(elems, index) };
Some(elem)
} else {
None
}
}
fn main() {
let elems = &[42];
let elem = get_elem_by_index(elems, 0);
dbg!(&elem);
}
И эта безопасная версия никогда не выстрелит по памяти, какие бы аргументы вы туда не передали. Если что, я не призываю вас писать подобный код на Rust (есть функция slice::get
), я показываю, как можно перейти из unsafe
подмножества Rust в безопасное подмножество с сохранением гарантий безопасности. На месте нашей unchecked_get_elem_by_index
могла быть аналогичная функция, написанная на C.
Благодаря межъязыковой LTO вызов сишной функции может быть абсолютно бесплатен:
C |
|
Rust |
|
asm |
|
Я выложил проект с флагами компилятора на гитхаб. Результирующий выхлоп ассемблера аналогичен коду, написанному на чистом C(link:godbolt), но имеет гарантии кода, написанного на Rust.
Миф №8. Безопасность Rust не доказана.
Антон (20:38):
Есть у нас замечательный язык программирования X. Этот язык программирования математически верифицируемый язык программирования. Если вдруг ваше приложение собралось и оно написано этом языке программирования X, то значит математически доказано, что в этом приложении нет ошибок. Звучит круто. Очень. Есть проблема. Мы используем сишные библиотеки, и когда мы их используем из такого языка программирования X, то разумеется все математические доказательства немного отваливаются.
В 2018 году доказали, что система типов Rust, механизмы заимствования, владения, времён жизни и многопоточности корректны. Так же было доказано, что если мы используем семантически правильный код из библиотек внутри unsafe
и смешаем это с синтаксически правильным safe
кодом, мы получим семантически правильный код, который не позволяет стрелять по памяти или делать гонки данных.
Из этого следует, что если вы подключаете и используете крейт(библиотеку), которая содержит unsafe, но предоставляет правильные безопасные обертки, то ваш код от этого не станет небезопасным.
В качестве практического применения своей модели авторы доказали корректность некоторых примитивов стандартной библиотеки, включая Mutex, RwLock, thread::spawn. А они используют сишные функции. Таким образом, в Rust невозможно случайно расшарить переменную между потоков без примитивов синхронизации; а, используя Mutex из стандартной библиотеки, доступ к переменной всегда будет корректен, несмотря на то, что их реализация опирается на сишные функции. Круто? Круто.
Заключение
Объективно обсуждать относительные преимущества того или иного языка сложно, особенно если вам сильно нравится один язык и не нравится другой. Весьма часто новый апологет очередного "новоявленного языка-убийцы C++" делает громкие заявления, не разобравшись толком с C++, за что ожидаемо получает по рукам.
Однако от признанных экспертов я ожидаю взвешенного освещения ситуации, которое, как минимум, не содержит грубых фактических ошибок.
Большое спасибо Дмитрию Кашицыну и Алексею Кладову за ревью статьи.
ncr
И напрасно.
Эксперты — они потому и эксперты, что узкая специализация.
Чем больше времени потрачено на что-то одно, тем меньше его на всё остальное.
Чем больше набито шишек, тем больше предубеждение, что «там» всё еще хуже и нежелание набивать их еще раз. Всяк кулик своё болото хвалит.
В качестве канонічного примера можно почитать спичи Линуса о C++.
Halt
Есть такая штука, называется эффект Даннинга Крюгера. И смысл у него в том, что чем больше человек знает, тем более осторожны его суждения в своей и особенно в смежных областях. Хвалить свое болото и быть некомпетентным в чужом — это несколько разные вещи. Линус топит плюсы не потому что они плохи сами по себе, а потому что их пытаются регулярно пихать в ядро, где, по его месту, им не место.
Я так понял, что Роман выступает не «за» или «против» а за профессионализм и за научный подход.
Gorthauer87
Аргументы Линуса вполне осмысленны, потому что многие плюсовые абстракции не zero cost все же, но это все вполне решаемо адекватным стайл гайдом. Другое дело, что он явно не хочет с этим заморачиваться. Впрочем, не помню, чтобы он хейтил раст в этом же контексте.
khim
Подавляющий по объёму (но, конечно, не по важности) объём кода Linux написан людьми, которые имеют слабое представление о C и C++ в прицнипе — они вообще железячники, из просто драйвера нужны, чтобы железяку продать.
Когда они карго-култят драйвер дёргая куски C кода — результат получается слегка вменяемым и его можно, итерационно, довести до чего-то разумного.
Когда они чего-то напишут с полным игнорированием Style Guide на C++ — это можно будет только выкинуть и переписать с нуля. Кто будет это делать?
wigneddoom
Очень сильное заявление, особенно, что люди имеют слабое представление о Си. Пример, приведёте из кода?
gecube
Memory leak в драйвере megaraid. Могли быть устранены в зародыше — при внимательном и вдумчивом написании кода. Либо прогоне kmemleak на рабочей системе. Пришлось патчить самому
wigneddoom
Отлично, баги есть, есть откровенный говнокод. Но вы тоже будете обобщать, что среди разработчиков Linux люди не понимают Си.
P/S. Я не согласен с утверждением: «людьм, которые имеют слабое представление о Си»Я даже не спорю о их представлениях о С++ или rust или python.
gecube
Погодите. Вы просили пример. Я привел. Если бы разработчики этого конкретного драйвера были чуточку "умнее среднего" — проблемы не было. Но она есть. Никаких обобщений я не делал. Вы меня с кем-то перепутали
wigneddoom
Нет, возможно я вас ввёл в заблуждение, но я просил пример не понимания Си. Не баг, а именно не понимание Си.
gecube
баг — это следствие (развожу руками):
и они писали бы корректный код (не синтаксически, а логически)
khim
Посмотрите на реакцию на любой vendor-драйвер в LKML. Из последнего — дискуссию про exFAT от Samsung можете посмотреть.
wigneddoom
Т.е вы мне предлагаете посмотреть дискуссию, где обсуждаются конкретные проблемы драйвера exfat. Но при этом утверждаете, что люди не понимают в Си?
khim
Потому что когда по неизвестно какому разу обсуждается что нельзя проверять на переполнение с помощью проверки
x + 100 < x
— то это оно и есть.knstqq
почему? Для беззнаковых чисел именно так и можно проверять (если тип не будет расширен до int с unsigned char)
netch80
И это тоже нехорошо: получается, что беззнаковая арифметика хуже оптимизируется, чем знаковая, по причине тараканов 40-летней давности.
khim
Ну вот если бы там речь шла только про беззнаковые числа — вопросов бы не было.
atomlib
Начнём с того, что мужика звали не «Даннинг-Крюгер». Это два разных исследователя, их фамилии пишутся через тире с пробелами: «эффект Даннинга — Крюгера».
Оригинальное исследование 1999 года заключалось в следующем: студентам раздали опросники на разные темы. Также студенты оценили, насколько хорошо они ответили.
Студентов разбили на квартили по успешности ответов. Получилось что-то такое:
То есть всё полностью наоборот: чем лучше себя человек оценивал, тем лучше он себя проявил. При этом в среднем первая квартиль пусть и отвечала хуже второй, но оценила себя ниже, вторая — хуже третьей, но и оценила себя ниже третьей, и так далее.
Да, первая, квартиль отвечала куда хуже, чем ожидала сама. Но это не даёт право в Интернете затыкать рты всем, демонстрирующим уверенность в чём-либо: «У вас Даннинг-Крюгер, азаза».
Само исследование невнятное: какие-то статистические выкладки с регрессией к среднему. Выше был дан график для опросника по юмору. График для опросника по логике выглядит более непонятно:
Каких-то больших различий нет: 5—10 процентов. При этом хорошо себя оценили как те, кто плохо ответил, так и лучшие. Получается, что если человек в себе уверен, то это либо профан, либо эксперт?
Я тут разве что могу заключить, что все люди себя оценивают выше среднего. Вот это интересно.
В 2006 году исследование повторяли. Результаты совсем невнятные. Люди себя оценивают хуже, если тест был сложный?
Нет никаких эффектов. Если вы с кем-то не согласны, то нужно опровергать чужую точку зрения.