asm gcc / clang이 x86에 대해 생성하는 것에 대한 자세한 내용은 다른 회전 질문에 대한이 답변 의 이전 버전을 참조하십시오 .
Undefined Behavior를 피하는 C 및 C ++에서 회전을 표현하는 가장 컴파일러 친화적 인 방법은 John Regehr의 구현 인 것 같습니다 . 유형의 너비에 따라 회전하도록 조정했습니다 (예 : 고정 너비 유형 사용 uint32_t
).
#include <stdint.h> // for uint32_t
#include <limits.h> // for CHAR_BIT
#include <assert.h>
static inline uint32_t rotl32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1);
c &= mask;
return (n<<c) | (n>>( (-c)&mask ));
}
static inline uint32_t rotr32 (uint32_t n, unsigned int c)
{
const unsigned int mask = (CHAR_BIT*sizeof(n) - 1);
c &= mask;
return (n>>c) | (n<<( (-c)&mask ));
}
뿐만 아니라 부호없는 정수 유형에 대해 작동 uint32_t
하므로 다른 크기의 버전을 만들 수 있습니다.
static_assert
예를 들어 일부 24 비트 DSP 또는 36 비트 메인 프레임에서는 그렇지 않은 많은 안전 검사 ( 유형 너비가 2의 거듭 제곱 인 경우 포함)가 포함 된 C ++ 11 템플릿 버전도 참조하십시오 .
회전 너비를 명시 적으로 포함하는 이름을 가진 래퍼의 백엔드로만 템플릿을 사용하는 것이 좋습니다. 정수 승격 규칙은 rotl_template(u16 & 0x11UL, 7)
16 비트가 아닌 32 비트 또는 64 비트 회전을 수행함을 의미합니다 (의 너비에 따라 다름 unsigned long
). 심지어 uint16_t & uint16_t
로 승격 signed int
플랫폼에서 제외하고, C ++의 정수 프로모션 규칙에 의해 int
보다 넓은입니다 uint16_t
.
x86 에서이 버전 은 x86 회전 및 시프트 명령어 가 C 소스와 동일한 방식으로 시프트 카운트를 마스크 한다는 것을 컴파일러가 알고 있기 때문에 컴파일하는 컴파일러가 있는 단일rol r32, cl
(또는 rol r32, imm8
)에 인라인됩니다 .
x86에서이 UB 회피 관용구에 대한 컴파일러 지원 uint32_t x
및 unsigned int n
가변 카운트 시프트 :
- clang : clang3.5 이후 가변 횟수 회전, 그 전에 여러 시프트 + 또는 insns로 인식됩니다.
- gcc : gcc4.9 이후 가변 카운트 회전 , 그 전에 여러 시프트 + 또는 insns로 인식됩니다. gcc5 이상은 위키피디아 버전에서도 변수 개수에 대해
ror
또는 rol
명령어를 사용하여 분기와 마스크를 최적화 합니다.
- icc : ICC13 또는 이전 버전부터 가변 횟수 회전에 지원됩니다 . 상수 카운트
shld edi,edi,7
는 rol edi,7
BMI2를 rorx eax,edi,25
MOV 저장에 사용할 수없는 경우 일부 CPU (특히 AMD, 일부 Intel) 보다 느리고 더 많은 바이트를 사용 하는 회전 사용 입니다 .
- MSVC : x86-64 CL19 : 고정 카운트 회전에 대해서만 인식됩니다. (wikipedia 관용구는 인식되지만 분기 및 AND는 최적화되지 않습니다.) x86 (x86-64 포함) 에서
_rotl
/ _rotr
내장 함수를 사용합니다 <intrin.h>
.
ARM 용 GCC 사용은 and r1, r1, #31
가변 수의 회전에 대한 있지만, 여전히 단일 명령과 실제 회전을한다 : ror r0, r0, r1
. 따라서 gcc는 회전 횟수가 본질적으로 모듈 식이라는 것을 인식하지 못합니다. ARM 문서에서 "시프트 길이가있는 ROR n
,, 32 이상은 시프트 길이가있는 ROR과 동일합니다 n-32
"라고 말합니다 . ARM의 왼쪽 / 오른쪽 시프트가 카운트를 포화시키기 때문에 gcc가 혼란스러워서 32 이상 시프트하면 레지스터가 지워집니다. (x86과 달리, 시프트는 회전과 동일하게 카운트를 마스킹합니다). 회전 관용구를 인식하기 전에 AND 명령이 필요하다고 결정했을 것입니다. 해당 대상에서 비 원형 시프트가 작동하는 방식 때문입니다.
현재 x86 컴파일러는 여전히 추가 명령어를 사용하여 8 비트 및 16 비트 회전에 대한 변수 수를 마스킹합니다. 아마도 ARM에서 AND를 피하지 않는 이유와 같습니다. 성능이 x86-64 CPU의 회전 수에 의존하지 않기 때문에 이것은 놓친 최적화입니다. (카운트 마스킹은 성능상의 이유로 286으로 도입되었습니다. 현대 CPU와 같은 일정한 지연 시간이 아니라 반복적으로 시프트를 처리했기 때문입니다.)
BTW는 가변 카운트 회전에 대해 오른쪽 회전을 선호하므로 컴파일러가 32-n
오른쪽 회전 만 제공하는 ARM 및 MIPS와 같은 아키텍처에서 왼쪽 회전을 구현 하지 않도록합니다 . (이는 컴파일 시간 상수 카운트로 최적화됩니다.)
재미있는 사실 : ARM 정말 전용 이동 / 회전 명령이없는, 그것은 그냥 MOV의 소스 피연산자 ROR 모드에서 배럴 쉬프터를 거치지가 : mov r0, r0, ror r1
. 따라서 회전은 EOR 명령 또는 기타에 대한 레지스터 소스 피연산자로 접을 수 있습니다.
n
및 반환 값에 대해 서명되지 않은 유형을 사용하는지 확인하십시오. 그렇지 않으면 rotate가 아닙니다 . (x86 타겟에 대한 gcc는 산술 오른쪽 시프트를 수행하여 0이 아닌 부호 비트의 복사본으로 OR
시프트하므로 두 값을 함께 시프트 할 때 문제가 발생합니다 . 음의 부호있는 정수의 오른쪽 시프트는 C에서 구현 정의 동작입니다.)
또한 부호있는 유형(-n)&31
이 1의 보수 또는 부호 / 크기가 될 수 있고 부호없는 또는 2의 보수로 얻는 모듈 식 2 ^ n과 동일하지 않기 때문에 시프트 카운트가 부호없는 유형인지 확인하십시오 . (Regehr의 블로그 게시물에 대한 의견 참조). unsigned int
모든 너비에 대해 내가 본 모든 컴파일러에서 잘 작동합니다 x
. 일부 다른 유형은 실제로 일부 컴파일러의 관용구 인식을 무력화하므로 동일한 유형을 x
.
일부 컴파일러는 회전에 대한 내장 함수를 제공 하는데, 이식 가능한 버전이 대상 컴파일러에서 좋은 코드를 생성하지 않는 경우 inline-asm보다 훨씬 좋습니다. 내가 아는 컴파일러에는 크로스 플랫폼 내장 함수가 없습니다. 다음은 몇 가지 x86 옵션입니다.
#if defined(__x86_64__) || defined(__i386__)
#ifdef _MSC_VER
#include <intrin.h>
#else
#include <x86intrin.h> // Not just <immintrin.h> for compilers other than icc
#endif
uint32_t rotl32_x86_intrinsic(rotwidth_t x, unsigned n) {
return _rotl(x, n);
}
#endif
x86이 아닌 일부 컴파일러에도 내장 함수가있을 수 있지만이 커뮤니티 위키 답변을 확장하여 모두 포함하지 않도록하겠습니다. (아마도 intrinsics 에 대한 기존 답변 에서 그렇게 할 수 있습니다 ).
(이 답변의 이전 버전은 MSVC 특정 인라인 asm (32 비트 x86 코드에서만 작동) 또는 C 버전의 경우 http://www.devx.com/tips/Tip/14043 을 제안 했습니다. 댓글은 이에 대한 답변입니다. .)
인라인 asm 은 입력이 저장 / 다시로드되도록 강제하기 때문에 특히 MSVC 스타일 과 같은 많은 최적화 를 무력화 합니다. 신중하게 작성된 GNU C 인라인 asm 회전은 카운트가 컴파일 시간 상수 시프트 카운트에 대한 즉각적인 피연산자가 될 수 있도록 허용하지만 시프트 할 값이 컴파일 타임 상수 인 경우에도 완전히 최적화 할 수 없습니다. 인라인 후. https://gcc.gnu.org/wiki/DontUseInlineAsm .