Este problema rastreia o desfazer do padrão -Zmutable-alias=no
introduzido em https://github.com/rust-lang/rust/pull/54639 por conta de um bug no LLVM. cc @nagisa
( Deja vu? )
Ainda estou tentando descobrir o problema subjacente. O ingresso interessante é https://github.com/rust-lang/rust/issues/54462.
Usando a reprodução mínima de @nagisa :
Caso de teste minimizado sem código inseguro (certifique-se de compilar com 1 unidade codegen!):
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);
}
Consegui dividir os passos de otimização do LLVM para encontrar o que estava causando o erro.
A execução deste comando resulta em um executável funcional (substitua bug.rs
pelo nome do arquivo em que você salvou a reprodução).
rustc -Z no-parallel-llvm -C codegen-units=1 -O -Z mutable-noalias=yes -C llvm-args=-opt-bisect-limit=2260 bug.rs
Ao executar este comando resulta em um executável quebrado (o `assert_eq`` falha):
rustc -Z no-parallel-llvm -C codegen-units=1 -O -Z mutable-noalias=yes -C llvm-args=-opt-bisect-limit=2261 bug.rs
Para este arquivo, a otimização 2261
corresponde a Global Value Numbering on function (_ZN3bug6swappy17hdcc51d0e284ea38bE)
A divisão das revisões do LLVM (usando a divisão do llvmlab) reduz para r305936-r305938, presumivelmente r305938:
[BasicAA] Use MayAlias em vez de PartialAlias para fallback.
Observe que esta é uma mudança bastante antiga, de junho de 2017.
Edit: Olhando para a descrição do commit, parece provável que o bug existia antes disso, mas foi mascarado pelo BasicAA impedindo que passagens de alias posteriores rodassem, que é o que o commit corrigiu. O caso envolve a verificação de aliasing entre um par de instruções getelementptr
onde o compilador sabe que eles têm o mesmo endereço de base, mas não conhece os deslocamentos.
Edit2: Além disso, passar -enable-scoped-noalias=false
como uma opção LLVM evita a compilação incorreta. (Isso não é surpreendente, uma vez que desativa totalmente o manuseio de noálias, mas apenas no caso de ajudar ...)
De uma olhada no IR pré-GVN, eu sinto que a causa raiz aqui pode estar no desenrolar do loop, dependendo se meu entendimento de como as anotações de aliasing do LLVM funcionam está correto.
Considere um código como
int *a, *b;
for (int i = 0; i < 4; i++) {
a[i & 1] = b[i & 1];
}
onde a[i & 1]
e b[i & 1]
não fazem alias dentro de uma única iteração , mas a
e b
em geral podem ser alias.
No LLVM IR isso seria algo como:
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}
Se executarmos isso em -loop-unroll
, obteremos:
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}
Observe como todas as quatro cópias do loop usam metadados de aliasing no mesmo domínio de aliasing. Em vez de ser noalias em uma única iteração, é noalias em toda a função.
Finalmente, -scoped-noalias -gvn
nos dá:
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}
E isso resultará em resultados incorretos se a = b + 1
.
É possível reproduzir esse problema de C com o seguinte código:
#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;
}
Com o Clang 6.0, isso imprime 2 2 2
em -O0
e 1 2 2
em -O3
. Não tenho certeza se este código é legal sob a semântica restrict
em C, mas acho que deveria ser legal sob a semântica noalias
mais estrita do LLVM.
Eu o reduzi a um caso de teste C simples (compilar em -O3 e -O0 e comparar a saída):
#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]);
}
Observe que se você remover restrict
, o equivalente em C de noalias
, o comportamento está correto. Ainda assim, o assert(a != b)
passa, provando que nenhum UB pode ocorrer devido a chamá-lo com restrict
.
O que está acontecendo é:
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]
não pode ser alternado com mat[idxs[1]]
ou mat[1]
, logo não pode ter sido alterado entre mat[0] = 7;
e mat[idxs[2]] = mat[0];
, logo é seguro para numeração de valor global para otimizar o último para mat[idxs[2]] = 7;
.Mas mat[0]
faz alias com mat[idxs[1]]
, porque idxs[1] == 0
. E não prometemos que não faria isso, porque na segunda iteração, quando &mat[idxs[1]]
é passado para copy
, o outro argumento é &mat[1]
. Então, por que o LLVM acha que não pode?
Bem, tem a ver com a maneira copy
é embutido. O atributo de função noalias
é transformado em metadados !alias.scope
e !noalias
nas instruções de carregamento e armazenamento, como:
%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
Normalmente, se uma função é alinhada várias vezes, cada cópia obtém seus próprios IDs exclusivos para alias.scope e noalias, indicando que cada chamada representa sua própria relação de 'desigualdade' * entre o par de argumentos marcado noalias
( restrict
no nível C), que pode ter valores diferentes para cada chamada.
No entanto, neste caso, primeiro a função é embutida no loop, então o código embutido é duplicado quando o loop é desenrolado - e essa duplicação não altera os IDs. Por causa disso, o LLVM pensa que nenhum dos a
pode criar um alias com b
, que é falso, porque a
do primeiro e do terceiro chama aliases com b
da segunda chamada (todos apontando para &mat[0]
).
Surpreendentemente, o GCC também compila isso, com saída diferente. (clang e GCC em -O0 geram 7 10
; clang em -O3 geram 7 7
; GCC em -O3 geram 10 7
.) Uh, eu realmente espero que não afinal bagunçar alguma coisa e adicionar UB, mas não vejo como ...
* É um pouco mais complicado do que isso, mas neste caso, uma vez que copy
não usa nenhum ponteiro aritmético e escreve para ambos os ponteiros, a desigualdade a != b
é necessária e suficiente para uma chamada não para seja UB.
Heh, parece que corri com @nikic para encontrar a mesma explicação. O caso de teste deles é um pouco melhor :)
Esse é um momento muito bom ^^ Chegamos à mesma conclusão com quase o mesmo caso de teste reduzido ao mesmo tempo :)
Para corrigir isso, provavelmente algo na linha de https://github.com/llvm-mirror/llvm/blob/54d4881c352796b18bfe7314662a294754e3a752/lib/Transforms/Utils/InlineFunction.cpp#L801 também precisa ser feito em LoopUnrollPass.
Enviei um relatório de bug do LLVM para esse problema em https://bugs.llvm.org/show_bug.cgi?id=39282.
E - apenas mencionando isso para integridade - enviei um relatório de bug ao GCC, pois ele também compilou incorretamente meu caso de teste C: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=87609
Triagem: Se estou lendo corretamente, a correção LLVM foi aceita (https://reviews.llvm.org/D9375). Não tenho certeza do que isso significa para realmente mesclar ao LLVM, ou quando isso aconteceu; alguém com mais conhecimento sobre o processo de revisão do LLVM deve verificar se o problema foi corrigido agora (e para quais versões).
Não está mesclado, o processo de revisão foi um pouco estranho e a pessoa que deu o OK não revisou mais os patches.
Houve uma chamada para testes com o conjunto de patches "restrição total". Provavelmente seria valioso se alguém tentasse reativar o noalias no Rust em cima de um llvm que tenha esse conjunto de patch aplicado.
Estou disposto a tentar fazer isso.
Tenho acesso a servidores moderadamente poderosos (48 núcleos HT, 128G de ram) e acho que consigo construir tudo corretamente com o patch.
Assim que tiver um conjunto de ferramentas em funcionamento, quais caixas você recomendaria experimentar?
Terminei de mesclar todos os commits específicos de ferrugem no master upstream do llvm e apliquei o patch .
Aqui está o branch resultante: https://github.com/PaulGrandperrin/llvm-project/tree/llvm-master-with-rustlang-patches-and-D69542
Vou agora tentar compilar o conjunto de ferramentas e testá-lo
Primeiro, certifique-se de reverter: https://github.com/rust-lang/rust/pull/54639 para que Rust esteja realmente emitindo noalias.
Uma boa primeira coisa a tentar é algo como:
pub fn adds(a: &mut i32, b: &mut i32) {
*a += *b;
*a += *b;
}
e confirme se ele compila para algo como:
example::adds:
mov eax, dword ptr [rsi]
add eax, eax
add dword ptr [rdi], eax
ret
e não
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
Em seguida, certifique-se de que o código de https://github.com/rust-lang/rust/issues/54462#issue -362850708 não seja mais compilado incorretamente.
@jrmuizel Observe que # 54639 inclui o reprodutor mínimo de @nagisa para # 54462 como um novo teste de compilador. Talvez uma reversão total não esteja em ordem?
Eu não acho que seja importante incluí-lo ou não porque AFAIK se trata apenas de alterar algum sinalizador padrão (que pode ser substituído por -Zmutable-noalias=yes
) e posso compilar manualmente o arquivo de teste mencionado.
Só para você saber, ainda estou tentando construir o LLVM, mas agora recebo um erro de compilação com ou sem os patches específicos de ferrugem aplicados (ou seja: apenas o mestre llvm do upstream + o patch também falha):
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....
Talvez o mestre LLVM já tenha saído de sincronia com o patch (o que pode acontecer considerando o tamanho do patch e a velocidade com que o mestre LLVM está se movendo) e você precisa compilar com uma revisão anterior do mestre LLVM? Definitivamente valeria a pena pingar o autor do patch sobre isso.
É exatamente o que estou fazendo, tentando encontrar uma revisão mais antiga que seja compilada com o patch :-)
Agora tenho certeza de que o problema não é com llvm / master, mas sim com o patch.
O commit mais antigo de llvm / master que ainda é compatível com o patch é https://github.com/llvm/llvm-project/commit/5b99c189b3bfc0faa157f7ca39652c0bb8c315a7, mas mesmo naquele período o patch falha ao compilar.
Estou muito cansado e com preguiça de tentar entender o C ++ agora, tentarei novamente amanhã.
Enquanto isso, alguém pode entrar em contato com o autor do patch para pedir ajuda?
Eu não acho que você será capaz de usar facilmente o LLVM master com Rust (depois de realmente compilá-lo) sem corrigir o rustllvm . AFAIK suporta apenas versões 6-9 no momento.
@ mati865 Tentei primeiro aplicar o patch na bifurcação llvm-9 do Rust, mas também não foi nada trivial ...
O patch aparentemente foi realocado em llvm / llvm-project @ 82d3ba87d06f9e2abc6e27d8799587d433c56630. Será que funciona para você se você se inscrever além disso?
@jrmuizel obrigado, vou tentar isso!
Nesse ínterim, consegui adaptar com sucesso o rustllvm para compilar com o mestre do llvm.
Ping @PaulGrandperrin , alguma atualização?
Falando realisticamente, qual é o cronograma estimado e as chances gerais desse patch ser mesclado, considerando seu tamanho?
@MSxDOS Você gostaria de perguntar aos desenvolvedores LLVM. Em geral, eu esperaria que o tamanho de um patch importasse menos do que o desejo dos proprietários de vê-lo mesclado, então a pergunta a fazer é quanto o LLVM deseja vê-lo cair.
Aqui está o status mais recente que vi: https://reviews.llvm.org/D69542#1836439
Presumo que em algum momento deixará de ser relevante se https://github.com/bytecodealliance/cranelift~~ https://github.com/bjorn3/rustc_codegen_cranelift funcionar?
@leeoniya , não há um plano de curto prazo para usar cranelift para compilações opt. O cranelift não tem muito trabalho de otimização que seria necessário para tornar isso possível.
Fiquei surpreso ao descobrir o quão conservador o compilador é em relação a assumir que poderia haver aliasing sem essa opção. Por exemplo:
fn baz(s: &mut S) {
if s.y < 10 {
s.x = foo();
}
if s.y < 5 {
s.x = foo();
}
}
Como ele acessa os membros da estrutura por meio de &mut
, ele assume que s.x
e s.y
podem alias, portanto, requer dois acessos à memória em vez de um para s.y
. Isso é realmente lamentável, quando você considera quantas vezes o membro lê / escreve via &mut
deve ser intercalado em um programa típico.
Editar: com base em alguns testes, isso não parece afetar todas as leituras / gravações, o que não é surpreendente, porque provavelmente mataria o desempenho se isso acontecesse. Ainda assim, usar -Z mutable-noalias
corrige o duplo acesso à memória no exemplo acima, portanto, alguns casos podem ser interrompidos.
@PaulGrandperrin há uma nova versão deste patch em https://reviews.llvm.org/D69542 baseado em llvm @ 9fb46a452d4e5666828c95610ceac8dcd9e4ce16. Você está disposto a tentar colocá-lo em execução novamente?
Comentários muito úteis
Eu o reduzi a um caso de teste C simples (compilar em -O3 e -O0 e comparar a saída):
Observe que se você remover
restrict
, o equivalente em C denoalias
, o comportamento está correto. Ainda assim, oassert(a != b)
passa, provando que nenhum UB pode ocorrer devido a chamá-lo comrestrict
.O que está acontecendo é:
mat[0]
não pode ser alternado commat[idxs[1]]
oumat[1]
, logo não pode ter sido alterado entremat[0] = 7;
emat[idxs[2]] = mat[0];
, logo é seguro para numeração de valor global para otimizar o último paramat[idxs[2]] = 7;
.Mas
mat[0]
faz alias commat[idxs[1]]
, porqueidxs[1] == 0
. E não prometemos que não faria isso, porque na segunda iteração, quando&mat[idxs[1]]
é passado paracopy
, o outro argumento é&mat[1]
. Então, por que o LLVM acha que não pode?Bem, tem a ver com a maneira
copy
é embutido. O atributo de funçãonoalias
é transformado em metadados!alias.scope
e!noalias
nas instruções de carregamento e armazenamento, como:Normalmente, se uma função é alinhada várias vezes, cada cópia obtém seus próprios IDs exclusivos para alias.scope e noalias, indicando que cada chamada representa sua própria relação de 'desigualdade' * entre o par de argumentos marcado
noalias
(restrict
no nível C), que pode ter valores diferentes para cada chamada.No entanto, neste caso, primeiro a função é embutida no loop, então o código embutido é duplicado quando o loop é desenrolado - e essa duplicação não altera os IDs. Por causa disso, o LLVM pensa que nenhum dos
a
pode criar um alias comb
, que é falso, porquea
do primeiro e do terceiro chama aliases comb
da segunda chamada (todos apontando para&mat[0]
).Surpreendentemente, o GCC também compila isso, com saída diferente. (clang e GCC em -O0 geram
7 10
; clang em -O3 geram7 7
; GCC em -O3 geram10 7
.) Uh, eu realmente espero que não afinal bagunçar alguma coisa e adicionar UB, mas não vejo como ...* É um pouco mais complicado do que isso, mas neste caso, uma vez que
copy
não usa nenhum ponteiro aritmético e escreve para ambos os ponteiros, a desigualdadea != b
é necessária e suficiente para uma chamada não para seja UB.