llvm / llvm/llvm-project

[LoopIdiomRecognize] wcslen idiom miscompiles loop with unaligned wide loads

Open
#216,045 3 comments 1 reaction 0 assignees View on GitHub
confirmed loopoptim miscompilation regression:21
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Summary

Starting with LLVM/Clang 21, `LoopIdiomRecognize` recognizes a loop performing `wchar_t`-sized loads via `memcpy` and replaces it with `wcslen`.

The original loop supports unaligned source addresses because the loads are performed via `memcpy`. The generated `wcslen` call is passed the same unaligned address, seems to introduce a stronger alignment requirement and results in wrong code.

Disabling only the `wcslen` loop idiom transformation with `-mllvm -disable-loop-idiom-wcslen` fixes the result.

The storage is an actual `unsigned char` array.
All pointer arithmetic is performed on `unsigned char*` and remains within that array.
The source program does not create or dereference an unaligned `wchar_t*`.
Each `wchar_t` value is copied from the byte buffer with `memcpy` into a properly aligned local `wchar_t` object

## Reproducer

```c++
#include
#include
#include

#if defined(__clang__) || defined(__GNUC__)
__attribute__((noinline))
#endif
static std::size_t get_size(const unsigned char* bytes)
{
std::size_t i = 0;
wchar_t ch{};
do {

std::memcpy(
&ch,
bytes + i * sizeof(wchar_t),
sizeof(wchar_t)
);

++i;
} while (ch != wchar_t{}) ;
return i * sizeof(wchar_t);
}

int main()
{
alignas(wchar_t) static unsigned char aligned_buf[256] = {};
alignas(wchar_t) static unsigned char unaligned_buf[256] = {};

constexpr wchar_t ws[] = L"test str";
static_assert(sizeof(ws) == 9 * sizeof(wchar_t));

constexpr std::size_t offset = 1;

std::memcpy(
aligned_buf,
ws,
sizeof(ws)
);

std::memcpy(
unaligned_buf + offset,
ws,
sizeof(ws)
);

const unsigned char* aligned =
aligned_buf;

const unsigned char* unaligned =
unaligned_buf + offset;

std::printf(
"alignof(wchar_t) = %zu\n"
"aligned: address %% alignment = %zu\n"
"aligned: get_size = %zu, expected = %zu\n"
"unaligned: address %% alignment = %zu\n"
"unaligned: get_size = %zu, expected = %zu\n" ,

alignof(wchar_t),
reinterpret_cast(aligned) % alignof(wchar_t),
get_size(aligned), sizeof(ws),
reinterpret_cast(unaligned) % alignof(wchar_t),
get_size(unaligned), sizeof(ws)
);
}
```

## Output

**clang++ -O2**

```
alignof(wchar_t) = 4
aligned: address % alignment = 0
aligned: get_size = 36, expected = 36
unaligned: address % alignment = 1
unaligned: get_size = 32, expected = 36
```

**clang++ -O2 -mllvm -disable-loop-idiom-wcslen**

```
alignof(wchar_t) = 4
aligned: address % alignment = 0
aligned: get_size = 36, expected = 36
unaligned: address % alignment = 1
unaligned: get_size = 36, expected = 36
```

## Godbolt
https://godbolt.org/z/x9o55r9av

## Environment

- Target: x86-64 Linux
- Optimization level: `-O2`
- Affected compiler:
LLVM/Clang 21
LLVM/Clang trunk
- Workaround / control flag: `-mllvm -disable-loop-idiom-wcslen`

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the issue with the provided C++ program using clang++ -O2, then inspect the LoopIdiomRecognize wcslen transformation and its handling of alignment. Compare the generated result with -mllvm -disable-loop-idiom-wcslen. Done means the transformation preserves the correct result for the unaligned buffer while retaining the intended optimization for aligned input.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.