Эта проблема отслеживает отмену значения по умолчанию -Zmutable-alias=no
введенного в https://github.com/rust-lang/rust/pull/54639 из-за ошибки в LLVM. cc @nagisa
( Дежавю? )
Я все еще работаю над выяснением основной проблемы. Интересный билет - https://github.com/rust-lang/rust/issues/54462.
Используя минимальное воспроизведение @nagisa :
Минимизированный тестовый пример без небезопасного кода (убедитесь, что скомпилирован с 1 модулем кодогенератора!):
fn linidx(row: usize, col: usize) -> usize {
row * 1 + col * 3
}
fn swappy() -> [f32; 12] {
let mut mat = [1.0f32, 5.0, 9.0, 2.0, 6.0, 10.0, 3.0, 7.0, 11.0, 4.0, 8.0, 12.0];
for i in 0..2 {
for j in i+1..3 {
if mat[linidx(j, 3)] > mat[linidx(i, 3)] {
for k in 0..4 {
let (x, rest) = mat.split_at_mut(linidx(i, k) + 1);
let a = x.last_mut().unwrap();
let b = rest.get_mut(linidx(j, k) - linidx(i, k) - 1).unwrap();
::std::mem::swap(a, b);
}
}
}
}
mat
}
fn main() {
let mat = swappy();
assert_eq!([9.0, 5.0, 1.0, 10.0, 6.0, 2.0, 11.0, 7.0, 3.0, 12.0, 8.0, 4.0], mat);
}
Мне удалось разделить проходы оптимизации LLVM пополам, чтобы найти тот, который вызывает ошибку.
Выполнение этой команды приводит к работающему исполняемому файлу (замените bug.rs
именем файла, в котором вы сохранили репродукцию).
rustc -Z no-parallel-llvm -C codegen-units=1 -O -Z mutable-noalias=yes -C llvm-args=-opt-bisect-limit=2260 bug.rs
Во время выполнения этой команды исполняемый файл не работает (assert_eq не работает):
rustc -Z no-parallel-llvm -C codegen-units=1 -O -Z mutable-noalias=yes -C llvm-args=-opt-bisect-limit=2261 bug.rs
Для этого файла оптимизация 2261
соответствует Global Value Numbering on function (_ZN3bug6swappy17hdcc51d0e284ea38bE)
Разделение ревизий LLVM пополам (с использованием llvmlab bisect) сужает его до r305936-r305938, предположительно r305938:
[BasicAA] Используйте MayAlias вместо PartialAlias для отката.
Обратите внимание, что это довольно старое изменение с июня 2017 года.
Изменить: глядя на описание фиксации, кажется вероятным, что ошибка существовала до этого, но была замаскирована BasicAA, предотвращая запуск последующих передач псевдонима, что и было исправлено фиксацией. Этот случай включает проверку псевдонима между парой инструкций getelementptr
когда компилятор знает, что у них один и тот же базовый адрес, но не знает смещения.
Edit2: Кроме того, передача -enable-scoped-noalias=false
в качестве параметра LLVM предотвращает неправильную компиляцию. (Это неудивительно, поскольку это полностью отключает обработку noalias, но на всякий случай это помогает ...)
Глядя на IR до GVN, я чувствую, что основная причина здесь может заключаться в развертывании цикла, в зависимости от того, правильно ли я понимаю, как работают аннотации псевдонимов LLVM.
Рассмотрим код вроде
int *a, *b;
for (int i = 0; i < 4; i++) {
a[i & 1] = b[i & 1];
}
где a[i & 1]
и b[i & 1]
не являются псевдонимами в пределах одной итерации , но a
и b
в целом могут быть псевдонимами.
В LLVM IR это будет примерно так:
define void @test(i32* %addr1, i32* %addr2) {
start:
br label %body
body:
%i = phi i32 [ 0, %start ], [ %i2, %body ]
%j = and i32 %i, 1
%addr1i = getelementptr inbounds i32, i32* %addr1, i32 %j
%addr2i = getelementptr inbounds i32, i32* %addr2, i32 %j
%x = load i32, i32* %addr1i, !alias.scope !2
store i32 %x, i32* %addr2i, !noalias !2
%i2 = add i32 %i, 1
%cmp = icmp slt i32 %i2, 4
br i1 %cmp, label %body, label %end
end:
ret void
}
!0 = !{!0}
!1 = !{!1, !0}
!2 = !{!1}
Если мы пропустим это через -loop-unroll
мы получим:
define void @test(i32* %addr1, i32* %addr2) {
start:
br label %body
body: ; preds = %start
%x = load i32, i32* %addr1, !alias.scope !0
store i32 %x, i32* %addr2, !noalias !0
%addr1i.1 = getelementptr inbounds i32, i32* %addr1, i32 1
%addr2i.1 = getelementptr inbounds i32, i32* %addr2, i32 1
%x.1 = load i32, i32* %addr1i.1, !alias.scope !0
store i32 %x.1, i32* %addr2i.1, !noalias !0
%x.2 = load i32, i32* %addr1, !alias.scope !0
store i32 %x.2, i32* %addr2, !noalias !0
%addr1i.3 = getelementptr inbounds i32, i32* %addr1, i32 1
%addr2i.3 = getelementptr inbounds i32, i32* %addr2, i32 1
%x.3 = load i32, i32* %addr1i.3, !alias.scope !0
store i32 %x.3, i32* %addr2i.3, !noalias !0
ret void
}
!0 = !{!1}
!1 = distinct !{!1, !2}
!2 = distinct !{!2}
Обратите внимание, как все четыре копии цикла используют метаданные псевдонима в одном и том же домене. Вместо того, чтобы быть noalias внутри одной итерации, это noalias для всей функции.
Наконец, -scoped-noalias -gvn
дает нам:
define void @test(i32* %addr1, i32* %addr2) {
start:
%x = load i32, i32* %addr1, !alias.scope !0
store i32 %x, i32* %addr2, !noalias !0
%addr1i.1 = getelementptr inbounds i32, i32* %addr1, i32 1
%addr2i.1 = getelementptr inbounds i32, i32* %addr2, i32 1
%x.1 = load i32, i32* %addr1i.1, !alias.scope !0
store i32 %x.1, i32* %addr2i.1, !noalias !0
store i32 %x, i32* %addr2, !noalias !0
store i32 %x.1, i32* %addr2i.1, !noalias !0
ret void
}
!0 = !{!1}
!1 = distinct !{!1, !2}
!2 = distinct !{!2}
И это приведет к неверным результатам, если a = b + 1
.
Эту проблему можно воспроизвести из C с помощью следующего кода:
#include "stdio.h"
void copy(int * restrict to, int * restrict from) {
*to = *from;
}
void test(int *a, int *b) {
for (int i = 0; i < 4; i++) {
copy(&b[i & 1], &a[i & 1]);
}
}
int main() {
int ary[] = {0, 1, 2};
test(&ary[1], &ary[0]);
printf("%d %d %d\n", ary[0], ary[1], ary[2]);
return 1;
}
В Clang 6.0 это печатает 2 2 2
в -O0
и 1 2 2
в -O3
. Я не уверен, что этот код законен для семантики restrict
в C, но я думаю, что он должен быть законным при более строгой семантике noalias
LLVM.
Я свел его к простому тесту C (скомпилируйте с -O3 и -O0 и сравните вывод):
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
__attribute__((always_inline))
static inline void copy(int *restrict a, int *restrict b) {
assert(a != b);
*b = *a;
*a = 7;
}
__attribute__((noinline))
void floppy(int mat[static 2], size_t idxs[static 3]) {
for (int i = 0; i < 3; i++) {
copy(&mat[i%2], &mat[idxs[i]]);
}
}
int main() {
int mat[3] = {10, 20};
size_t idxs[3] = {1, 0, 1};
floppy(mat, idxs);
printf("%d %d\n", mat[0], mat[1]);
}
Обратите внимание, что если вы удалите restrict
, эквивалент C noalias
, поведение будет правильным. Но даже тогда assert(a != b)
проходит, доказывая, что UB не может произойти из-за вызова его с помощью restrict
.
Что происходит:
for (int i = 0; i < 3; i++) {
mat[idxs[i]] = mat[i%2]; mat[i%2] = 7;
}
mat[idxs[0]] = mat[0]; mat[0] = 7; /* from copy(&mat[0%2], &mat[idxs[0]]) */
mat[idxs[1]] = mat[1]; mat[1] = 7; /* from copy(&mat[1%2], &mat[idxs[1]]) */
mat[idxs[2]] = mat[0]; mat[0] = 7; /* from copy(&mat[2%2], &mat[idxs[2]]) */
mat[0]
не может быть псевдонимом с mat[idxs[1]]
или mat[1]
, следовательно, его нельзя изменить между mat[0] = 7;
и mat[idxs[2]] = mat[0];
, следовательно, это безопасно для глобальная нумерация значений для оптимизации последнего до mat[idxs[2]] = 7;
.Но mat[0]
имеет псевдоним с mat[idxs[1]]
, потому что idxs[1] == 0
. И мы не обещали, что этого не произойдет, потому что на второй итерации, когда &mat[idxs[1]]
передается в copy
, другим аргументом является &mat[1]
. Так почему же LLVM думает, что это невозможно?
Ну, это связано со способом встраивания copy
. Атрибут noalias
function превращается в метаданные !alias.scope
и !noalias
в инструкциях загрузки и сохранения, например:
%8 = load i32, i32* %0, align 4, !tbaa !8, !alias.scope !10, !noalias !13
store i32 %8, i32* %7, align 4, !tbaa !8, !alias.scope !13, !noalias !10
store i32 7, i32* %0, align 4, !tbaa !8, !alias.scope !10, !noalias !13
Обычно, если функция встроена несколько раз, каждая копия получает свои собственные уникальные идентификаторы для alias.scope и noalias, что указывает на то, что каждый вызов представляет свое собственное отношение «неравенство» * между парой аргументов, отмеченных noalias
( restrict
на уровне C), которые могут иметь разные значения для каждого вызова.
Однако в этом случае сначала функция встроена в цикл, затем встроенный код дублируется при развертывании цикла - и это дублирование не меняет идентификаторы. Из-за этого LLVM считает, что ни один из a
может быть псевдонимом с каким-либо из b
, что неверно, потому что a
из псевдонимов первого и третьего вызовов с b
из второго вызова (все указывают на &mat[0]
).
Удивительно, но GCC также неправильно компилирует это с другим выводом. (clang и GCC при -O0 выводят 7 10
; clang at -O3 выводят 7 7
; GCC при -O3 выводят 10 7
.) Я очень надеюсь, что не облажаться и добавить UB все-таки, но я не понимаю, как ...
* Это немного сложнее, но в этом случае, поскольку copy
не использует арифметические операции с указателями и выполняет запись в оба указателя, неравенство a != b
необходимо и достаточно для того, чтобы вызов не выполнялся. быть UB.
Хех, похоже, я гонялся с @nikic, чтобы найти такое же объяснение. Их тестовый пример немного лучше :)
Это действительно отличное время ^^ Мы пришли к такому же выводу с почти таким же сокращенным тестовым примером в то же время :)
Чтобы исправить это, возможно, что-то вроде https://github.com/llvm-mirror/llvm/blob/54d4881c352796b18bfe7314662a294754e3a752/lib/Transforms/Utils/InlineFunction.cpp#L801 необходимо также выполнить в LoopUnrollPass.
Я отправил отчет об ошибке LLVM для этой проблемы по адресу https://bugs.llvm.org/show_bug.cgi?id=39282.
И - просто упомянув это для полноты - я отправил отчет об ошибке в GCC, поскольку он также неправильно скомпилировал мой тестовый пример C: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=87609
Сортировка: Если я правильно прочитал, исправление LLVM было принято (https://reviews.llvm.org/D9375). Я не уверен, что это значит для фактического слияния с LLVM и когда это произошло; Кто-то, более осведомленный о процессе ревизии LLVM, должен проверить, устранена ли проблема сейчас (и для каких версий).
Он не объединен, процесс проверки был немного странным, и человек, который одобрил его, больше не проверяет исправления.
Был призыв к тестированию с набором патчей «full restrict». Было бы, вероятно, полезно, если бы кто-нибудь попробовал повторно включить noalias в Rust поверх llvm, к которому применен этот набор патчей.
Я хочу попробовать это сделать.
У меня есть доступ к умеренно мощным серверам (48 ядер HT, 128 ГБ оперативной памяти), и я думаю, что смогу собрать все правильно с помощью патча.
Какие ящики вы бы порекомендовали попробовать, когда у меня будет рабочий набор инструментов?
Я закончил слияние всех коммитов, специфичных для ржавчины, на ведущем сервере llvm, а затем применил патч .
Вот получившаяся ветка: https://github.com/PaulGrandperrin/llvm-project/tree/llvm-master-with-rustlang-patches-and-D69542
Сейчас я попробую скомпилировать набор инструментов и попробовать
Сначала убедитесь, что вы вернулись: https://github.com/rust-lang/rust/pull/54639, чтобы Rust действительно генерировал noalias.
Первым делом можно попробовать что-нибудь вроде:
pub fn adds(a: &mut i32, b: &mut i32) {
*a += *b;
*a += *b;
}
и убедитесь, что он компилируется примерно так:
example::adds:
mov eax, dword ptr [rsi]
add eax, eax
add dword ptr [rdi], eax
ret
и не
example::adds:
mov eax, dword ptr [rdi]
add eax, dword ptr [rsi]
mov dword ptr [rdi], eax
add eax, dword ptr [rsi]
mov dword ptr [rdi], eax
ret
Затем убедитесь, что код из https://github.com/rust-lang/rust/issues/54462#issue -362850708 больше не компилируется неправильно.
@jrmuizel Обратите внимание , что # 54639 действительно включают @nagisa «s минимальный репродуктор для # 54462 в качестве нового теста компилятора. Может, полный откат не в порядке?
Я не думаю, что важно включать его или нет, потому что, AFAIK, речь идет только об изменении некоторого флага по умолчанию (который можно переопределить с помощью -Zmutable-noalias=yes
), и я могу вручную скомпилировать упомянутый тестовый файл.
Просто чтобы вы знали, я все еще пытаюсь создать LLVM, но на данный момент я получаю ошибку компиляции с примененными патчами, специфичными для ржавчины, или без них (то есть: только мастер llvm восходящего потока + патч тоже не работает):
In file included from /usr/include/c++/8/cmath:45,
from /opt/rust/src/llvm-project/llvm/include/llvm-c/DataTypes.h:28,
from /opt/rust/src/llvm-project/llvm/include/llvm/Support/DataTypes.h:16,
from /opt/rust/src/llvm-project/llvm/include/llvm/ADT/Hashing.h:47,
from /opt/rust/src/llvm-project/llvm/include/llvm/ADT/ArrayRef.h:12,
from /opt/rust/src/llvm-project/llvm/include/llvm/Transforms/Utils/NoAliasUtils.h:16,
from /opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:13:
/opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp: In function ‘void llvm::cloneNoAliasScopes(llvm::ArrayRef<llvm::MetadataAsValue*>, llvm::DenseMap<llvm::MDN
ode*, llvm::MDNode*>&, llvm::DenseMap<llvm::MetadataAsValue*, llvm::MetadataAsValue*>&, llvm::StringRef, llvm::LLVMContext&)’:
/opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:174:30: error: no matching function for call to ‘llvm::AliasScopeNode::AliasScopeNode(double)’
llvm::AliasScopeNode SNAN(MD);
^~~~
In file included from /opt/rust/src/llvm-project/llvm/include/llvm/IR/TrackingMDRef.h:16,
from /opt/rust/src/llvm-project/llvm/include/llvm/IR/DebugLoc.h:17,
from /opt/rust/src/llvm-project/llvm/include/llvm/IR/Instruction.h:21,
from /opt/rust/src/llvm-project/llvm/include/llvm/IR/BasicBlock.h:22,
from /opt/rust/src/llvm-project/llvm/include/llvm/IR/Instructions.h:27,
from /opt/rust/src/llvm-project/llvm/include/llvm/Transforms/Utils/NoAliasUtils.h:22,
from /opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:13:
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1446:12: note: candidate: ‘llvm::AliasScopeNode::AliasScopeNode(const llvm::MDNode*)’
explicit AliasScopeNode(const MDNode *N) : Node(N) {}
^~~~~~~~~~~~~~
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1446:12: note: no known conversion for argument 1 from ‘double’ to ‘const llvm::MDNode*’
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1445:3: note: candidate: ‘constexpr llvm::AliasScopeNode::AliasScopeNode()’
AliasScopeNode() = default;
^~~~~~~~~~~~~~
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1445:3: note: candidate expects 0 arguments, 1 provided
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1441:7: note: candidate: ‘constexpr llvm::AliasScopeNode::AliasScopeNode(const llvm::AliasScopeNode&)’
class AliasScopeNode {
^~~~~~~~~~~~~~
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1441:7: note: no known conversion for argument 1 from ‘double’ to ‘const llvm::AliasScopeNode&’
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1441:7: note: candidate: ‘constexpr llvm::AliasScopeNode::AliasScopeNode(llvm::AliasScopeNode&&)’
/opt/rust/src/llvm-project/llvm/include/llvm/IR/Metadata.h:1441:7: note: no known conversion for argument 1 from ‘double’ to ‘llvm::AliasScopeNode&&’
/opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:177:31: error: request for member ‘getName’ in ‘__builtin_nans(((const char*)""))’, which is of non-class ty
pe ‘double’
auto ScopeName = SNAN.getName();
^~~~~~~
/opt/rust/src/llvm-project/llvm/lib/Transforms/Utils/NoAliasUtils.cpp:187:39: error: request for member ‘getDomain’ in ‘__builtin_nans(((const char*)""))’, which is of non-class
type ‘double’
const_cast<MDNode *>(SNAN.getDomain()), Name);
^~~~~~~~~
[ 75%] Building CXX object lib/Target/Hexagon/CMakeFiles/LLVMHexagonCodeGen.dir/RDFCopy.cpp.o
make[2]: *** [lib/Transforms/Utils/CMakeFiles/LLVMTransformUtils.dir/build.make:635: lib/Transforms/Utils/CMakeFiles/LLVMTransformUtils.dir/NoAliasUtils.cpp.o] Error 1
make[2]: *** Waiting for unfinished jobs....
Может быть, мастер LLVM уже рассинхронизировался с патчем (что могло произойти, учитывая, насколько велик патч и как быстро движется мастер LLVM), и вам нужно использовать более старую версию мастера LLVM? Об этом определенно стоит поговорить с автором патча.
Это именно то, что я делаю, пытаясь найти более старую ревизию, которая собирается с патчем :-)
Теперь я уверен, что проблема не в llvm / master, а в патче.
Самая старая фиксация от llvm / master, которая все еще совместима с патчем, - это https://github.com/llvm/llvm-project/commit/5b99c189b3bfc0faa157f7ca39652c0bb8c315a7, но даже в этом случае патч не компилируется.
Я слишком устал и ленив, чтобы сейчас разбираться в C ++, завтра попробую еще раз.
А пока может кто-нибудь обратиться к автору патча за помощью?
Я не думаю, что вы сможете легко использовать мастер LLVM с Rust (после того, как вы его действительно соберете ), не исправляя
@ mati865 Сначала я попробовал наложить патч на вилку
Патч, по-видимому, переустановлен поверх llvm / llvm-project @ 82d3ba87d06f9e2abc6e27d8799587d433c56630. Будет ли это хорошо для вас, если вы подадите дополнительную заявку?
@jrmuizel спасибо, попробую!
Тем временем мне удалось успешно адаптировать rustllvm для сборки с помощью llvm master.
Пинг @PaulGrandperrin , есть обновления?
На самом деле, каковы предполагаемые сроки и общие шансы, что этот патч когда-либо будет объединен, учитывая его размер?
@MSxDOS Спросите разработчиков LLVM. В общем, я ожидал, что размер патча имеет меньшее значение, чем желание владельцев увидеть его слияние, поэтому возникает вопрос, насколько LLVM хочет, чтобы он приземлился.
Вот последний статус, который я видел: https://reviews.llvm.org/D69542#1836439
я предполагаю, что в какой-то момент это перестанет быть актуальным, если https://github.com/bytecodealliance/cranelift~~ https://github.com/bjorn3/rustc_codegen_cranelift сработает
@leeoniya , нет краткосрочного плана использовать кранлифт для опт билдов. Cranelift не требует той работы по оптимизации, которая могла бы сделать это возможным.
Я был удивлен, узнав, насколько консервативен компилятор в отношении предположения о возможном алиасинге без этой опции. Например:
fn baz(s: &mut S) {
if s.y < 10 {
s.x = foo();
}
if s.y < 5 {
s.x = foo();
}
}
Поскольку он обращается к членам структуры через &mut
, предполагается, что s.x
и s.y
могут быть псевдонимами, поэтому для s.y
требуется два доступа к памяти вместо одного. Это действительно прискорбно, если учесть, сколько раз в типичной программе необходимо чередовать чтение / запись члена через &mut
.
Изменить: на основе некоторого тестирования, похоже, это не влияет на все такие чтения / записи, что неудивительно, потому что это, вероятно, убило бы производительность, если бы это было. Тем не менее, использование -Z mutable-noalias
устраняет двойной доступ к памяти в приведенном выше примере, поэтому некоторые случаи могут быть остановлены.
@PaulGrandperrin есть новая версия этого патча на https://reviews.llvm.org/D69542, основанная на llvm @ 9fb46a452d4e5666828c95610ceac8dcd9e4ce16. Вы готовы попробовать запустить его снова?
Самый полезный комментарий
Я свел его к простому тесту C (скомпилируйте с -O3 и -O0 и сравните вывод):
Обратите внимание, что если вы удалите
restrict
, эквивалент Cnoalias
, поведение будет правильным. Но даже тогдаassert(a != b)
проходит, доказывая, что UB не может произойти из-за вызова его с помощьюrestrict
.Что происходит:
mat[0]
не может быть псевдонимом сmat[idxs[1]]
илиmat[1]
, следовательно, его нельзя изменить междуmat[0] = 7;
иmat[idxs[2]] = mat[0];
, следовательно, это безопасно для глобальная нумерация значений для оптимизации последнего доmat[idxs[2]] = 7;
.Но
mat[0]
имеет псевдоним сmat[idxs[1]]
, потому чтоidxs[1] == 0
. И мы не обещали, что этого не произойдет, потому что на второй итерации, когда&mat[idxs[1]]
передается вcopy
, другим аргументом является&mat[1]
. Так почему же LLVM думает, что это невозможно?Ну, это связано со способом встраивания
copy
. Атрибутnoalias
function превращается в метаданные!alias.scope
и!noalias
в инструкциях загрузки и сохранения, например:Обычно, если функция встроена несколько раз, каждая копия получает свои собственные уникальные идентификаторы для alias.scope и noalias, что указывает на то, что каждый вызов представляет свое собственное отношение «неравенство» * между парой аргументов, отмеченных
noalias
(restrict
на уровне C), которые могут иметь разные значения для каждого вызова.Однако в этом случае сначала функция встроена в цикл, затем встроенный код дублируется при развертывании цикла - и это дублирование не меняет идентификаторы. Из-за этого LLVM считает, что ни один из
a
может быть псевдонимом с каким-либо изb
, что неверно, потому чтоa
из псевдонимов первого и третьего вызовов сb
из второго вызова (все указывают на&mat[0]
).Удивительно, но GCC также неправильно компилирует это с другим выводом. (clang и GCC при -O0 выводят
7 10
; clang at -O3 выводят7 7
; GCC при -O3 выводят10 7
.) Я очень надеюсь, что не облажаться и добавить UB все-таки, но я не понимаю, как ...* Это немного сложнее, но в этом случае, поскольку
copy
не использует арифметические операции с указателями и выполняет запись в оба указателя, неравенствоa != b
необходимо и достаточно для того, чтобы вызов не выполнялся. быть UB.