Раньше считалось, что ARM-процессоры не смогли правильно обрабатывать непринятый доступ к памяти (ARMv5 и ниже). Что-то вроде u32 var32 = *(u32*)ptr;
просто сработает (повысит исключение), если ptr
не был правильно выровнен по 4 байтам.
Написание такого утверждения будет отлично работать для x86/x64, поскольку этот процессор всегда справлялся с такой ситуацией очень эффективно. Но, согласно стандарту C, это не "правильный" способ написать его. u32
, по-видимому, эквивалентен структуре из 4 байтов, которая должна быть выровнена на 4 байта.
Правильный способ достижения такого же результата при сохранении правильности правопорядка и обеспечении полной совместимости с любым процессором:
u32 read32(const void* ptr)
{
u32 result;
memcpy(&result, ptr, 4);
return result;
}
Это правильно, будет генерировать правильный код для любого процессора, который может или не читать на неустановленных позициях. Еще лучше, на x86/x64, он правильно оптимизирован для одной операции чтения, следовательно, имеет ту же производительность, что и первый оператор. Он портативный, безопасный и быстрый. Кто может спросить больше?
Ну, проблема в ARM, нам не очень повезло.
Написание версии memcpy
действительно безопасно, но, похоже, приводит к систематическим осторожным операциям, которые очень медленны для ARMv6 и ARMv7 (в основном, любого смартфона).
В ориентированном на производительность приложении, которое в значительной степени зависит от операций чтения, можно измерить разницу между 1-й и 2-й версиями: оно находится в 5x в настройках gcc -O2
. Это слишком много, чтобы его можно было игнорировать.
Попытка найти способ использования возможностей ARMv6/v7, я искал рекомендации по нескольким примерным кодам. Unfortunatley, похоже, они выбирают первое утверждение (прямой u32
доступ), который не должен быть правильным.
Это не все: новые версии GCC теперь пытаются реализовать автоматическую вектологию. На x64 это означает SSE/AVX, на ARMv7, что означает NEON. ARMv7 также поддерживает некоторые новые коды "Load Multiple" (LDM) и "Store Multiple" (STM), которые требуют выравнивания указателя.
Что это значит? Ну, компилятор может свободно использовать эти расширенные инструкции, даже если они не были специально вызваны из кода C (нет встроенных). Чтобы принять такое решение, он использует тот факт, что a u32* pointer
должен быть выровнен по 4 байтам. Если это не так, то все ставки отключены: undefined поведение, сбои.
Это означает, что даже на процессоре, поддерживающем неравнозначный доступ к памяти, теперь опасно использовать прямой доступ u32
, так как это может привести к генерации ошибочного кода при высоких настройках оптимизации (-O3
).
Итак, теперь это дилемма: как получить доступ к встроенной производительности ARMv6/v7 по негласному доступу к памяти без записи неправильной версии u32
access?
PS: Я также пробовал инструкции __packed()
, и с точки зрения производительности они, похоже, работают точно так же, как и метод memcpy
.
[Изменить]: Спасибо за отличные элементы, полученные до сих пор.
Посмотрев на сгенерированную сборку, я смог подтвердить @Notlikethat, что версия memcpy
действительно генерирует правильный код операции ldr
(невысокая загрузка). Однако я также обнаружил, что сгенерированная сборка бесполезно вызывает str
(command). Таким образом, полная операция теперь представляет собой невыровненную нагрузку, выровненное хранилище и конечную согласованную нагрузку. Это намного больше, чем нужно.
Отвечая @haneefmubarak, да, код правильно встроен. И нет, memcpy
очень далек от обеспечения максимально возможной скорости, так как принудительное принятие кода для прямого доступа u32
приводит к огромному приросту производительности. Поэтому существует какая-то лучшая возможность.
Большое спасибо @artless_noise. Ссылка на службу godbolt неоценима. Я никогда не мог так ясно видеть эквивалентность исходного кода C и его сборки. Это очень вдохновляет.
Я завершил один из примеров @artless, и он дает следующее:
#include <stdlib.h>
#include <memory.h>
typedef unsigned int u32;
u32 reada32(const void* ptr) { return *(const u32*) ptr; }
u32 readu32(const void* ptr)
{
u32 result;
memcpy(&result, ptr, 4);
return result;
}
после компиляции с использованием ARM GCC 4.8.2 при -O3 или -O2:
reada32(void const*):
ldr r0, [r0]
bx lr
readu32(void const*):
ldr r0, [r0] @ unaligned
sub sp, sp, #8
str r0, [sp, #4] @ unaligned
ldr r0, [sp, #4]
add sp, sp, #8
bx lr
Достаточно сказать....