Я обратился к коллеге, который if (i < input.size() - 1) print(0);
будет оптимизирован в этом цикле, чтобы input.size()
он не читался на каждой итерации, но оказалось, что это не так!
void print(int x) {
std::cout << x << std::endl;
}
void print_list(const std::vector<int>& input) {
int i = 0;
for (size_t i = 0; i < input.size(); i++) {
print(input[i]);
if (i < input.size() - 1) print(0);
}
}
Согласно Compiler Explorer с опциями gcc -O3 -fno-exceptions
мы фактически читаем input.size()
каждую итерацию и используем lea
для выполнения вычитания!
movq 0(%rbp), %rdx
movq 8(%rbp), %rax
subq %rdx, %rax
sarq $2, %rax
leaq -1(%rax), %rcx
cmpq %rbx, %rcx
ja .L35
addq $1, %rbx
Интересно, что в Rust такая оптимизация действительно происходит. Похоже, i
заменяется переменной, j
которая уменьшается на каждой итерации, а тест i < input.size() - 1
заменяется чем-то вроде j > 0
.
fn print(x: i32) {
println!("{}", x);
}
pub fn print_list(xs: &Vec<i32>) {
for (i, x) in xs.iter().enumerate() {
print(*x);
if i < xs.len() - 1 {
print(0);
}
}
}
В Compiler Explorer соответствующая сборка выглядит так:
cmpq %r12, %rbx
jae .LBB0_4
Я проверил, и я уверен, что r12
есть xs.len() - 1
и rbx
есть счетчик. Раньше есть add
для rbx
и за mov
пределами цикла в r12
.
Почему это? Похоже, что если GCC способен встроить size()
и, operator[]
как это было сделано, он должен быть в состоянии знать, что size()
не меняется. Но, может быть, оптимизатор GCC считает, что не стоит вытаскивать его в переменную? Или может быть есть какой-то другой возможный побочный эффект, который сделает это небезопасным - кто-нибудь знает?
println
, вероятно, это сложный метод, у компилятора могут возникнуть проблемы с доказательством того, чтоprintln
он не изменяет вектор.cout.operator<<()
. Компилятор не знает, что эта функция черного ящика не получает ссылку наstd::vector
глобальный объект.println
илиoperator<<
является ключевой.Ответы:
Не встроенный вызов функции
cout.operator<<(int)
- это черный ящик для оптимизатора (поскольку библиотека только что написана на C ++ и оптимизатор видит только прототип; см. Обсуждение в комментариях). Он должен предполагать, что любая память, на которую может указывать глобальная переменная, была изменена.(Или
std::endl
вызов. Кстати, зачем вызывать сброс в этой точке вместо того, чтобы просто печатать'\n'
?)Например, все, что он знает,
std::vector<int> &input
является ссылкой на глобальную переменную, и один из этих вызовов функций изменяет эту глобальную переменную . (Или где-то есть глобальный объектvector<int> *ptr
, или есть функция, которая возвращает указатель на astatic vector<int>
в каком-то другом модуле компиляции, или каким-то другим способом, которым функция может получить ссылку на этот вектор, не передавая ссылку на него нами.Если бы у вас была локальная переменная, адрес которой никогда не был взят, компилятор мог бы предположить, что вызовы не встроенных функций не могли изменить его. Потому что ни одна глобальная переменная не может содержать указатель на этот объект. ( Это называется анализом побега ). Вот почему компилятор может хранить
size_t i
в регистре вызовы функций. (int i
можно просто оптимизировать, потому что он скрытsize_t i
и не используется в противном случае).Это может сделать то же самое с локальным
vector
(то есть для указателей base, end_size и end_capacity.)ISO C99 имеет решение этой проблемы:
int *restrict foo
. Многие C ++ компилирует поддерживаетint *__restrict foo
обещание , что память , на которую указываетfoo
это только доступ через этот указатель. Чаще всего полезно в функциях, которые принимают 2 массива, и вы хотите пообещать компилятору, что они не перекрываются. Таким образом, он может автоматически векторизовать без генерации кода, чтобы проверить это и запустить запасной цикл.ОП комментарии:
Это объясняет, почему Rust может сделать эту оптимизацию, а C ++ - нет.
Оптимизация вашего C ++
Очевидно, вы должны использовать
auto size = input.size();
один раз в верхней части вашей функции, чтобы компилятор знал, что это инвариант цикла. Реализации C ++ не решают эту проблему за вас, поэтому вы должны сделать это сами.Вам также может понадобиться
const int *data = input.data();
поднять нагрузку указателя данных изstd::vector<int>
«блока управления». К сожалению, оптимизация может потребовать очень неидиоматических изменений источника.Rust - гораздо более современный язык, разработанный после того, как разработчики компиляторов узнали, что было возможно на практике для компиляторов. Это действительно показывает и другие способы, в том числе переносное раскрытие некоторых полезных вещей, которые процессоры могут делать через
i32.count_ones
, вращать, сканировать биты и т. Д. Действительно глупо, что ISO C ++ до сих пор не раскрывает ничего из этого, кромеstd::bitset::count()
.источник
operator<<
для этих типов операндов; поэтому в стандарте C ++ это не черный ящик, и компилятор может предположить, что он делает то, что говорится в документации. Может быть, они хотят поддержать разработчиков библиотек, добавив нестандартное поведение ...cout
позволяет объекту пользовательского класса, производному от которого,streambuf
быть связанным с использованием потокаcout.rdbuf
. Точно так же объект, полученный из,ostream
может быть связан сcout.tie
.this
указатель неявно передается. Это может произойти на практике уже в конструкторе. Рассмотрим этот простой цикл - я проверил только основной цикл gcc (отL34:
доjne L34
), но он определенно ведет себя так, как будто члены вектора экранированы (загружая их из памяти каждую итерацию).