C/C ++/RTL에 LDIR 기능과 같은 Z80이없는 이유는 무엇입니까?

StackOverflow https://stackoverflow.com/questions/387654

  •  23-08-2019
  •  | 
  •  

문제

Z80 머신 코드에서는 버퍼를 고정 값으로 초기화하는 저렴한 기술입니다. 따라서 코드의 덩어리가 다음과 같이 보일 수 있습니다.

LD HL, DESTINATION             ; point to the source
LD DE, DESTINATION + 1         ; point to the destination
LD BC, DESTINATION_SIZE - 1    ; copying this many bytes
LD (HL), 0X20                  ; put a seed space in the first position
LDIR                           ; move 1 to 2, 2 to 3...

결과는 목적지의 메모리 덩어리가 완전히 비워졌습니다. 나는 Memmove와 Memcpy를 실험 했으며이 행동을 복제 할 수 없습니다. Memmove가 올바르게 할 수 있다고 기대했습니다.

Memmove와 Memcpy가 왜 이런 식으로 행동합니까?

이러한 종류의 배열 초기화를 수행하는 합리적인 방법이 있습니까?

배열 초기화에 대한 char array [size] = {0}에 대해 이미 알고 있습니다.

나는 이미 Memset이 단일 캐릭터를 위해 일을 할 것이라고 이미 알고 있습니다.

이 문제에는 어떤 다른 접근법이 있습니까?

도움이 되었습니까?

해결책

나는 이것이 C와 C ++의 디자인 철학에 있다고 믿는다. 처럼 Bjarne Stroustrup 한 번 말했다, C ++ 디자인의 주요 안내 원칙 중 하나는 "사용하지 않는 것, 지불하지 않는 것"입니다. 그리고 데니스 리치 정확히 같은 단어로 말하지 않았을 수도 있습니다. 이제 메모리를 할당하면 자동으로 NULL에 초기화해야하며 동의하는 경향이 있다고 생각할 수 있습니다. 그러나 기계주기가 필요하며 모든주기가 중요한 상황에서 코딩하는 경우 허용되는 트레이드 오프가 아닐 수도 있습니다. 기본적으로 C와 C ++는 길을 벗어나려고 노력합니다. 따라서 초기화를 원한다면 직접해야합니다.

다른 팁

memmove 그리고 memcpy 메모리를 이동하거나 복사하는 데 유용한 의미가 아니기 때문에 그렇게 작동하지 마십시오. Z80에서 메모리를 채울 수있는 것은 편리하지만 "Memmove"라는 기능이 단일 바이트로 메모리를 채우는 이유는 무엇입니까? 메모리 블록을 움직이는 것입니다. 블록이 겹치는 방법에 관계없이 정답 (소스 바이트가 대상으로 이동)을 얻기 위해 구현되었습니다. 메모리 블록을 움직이는 정답을 얻는 것이 유용합니다.

메모리를 채우려면 원하는 것을 수행하도록 설계된 Memset을 사용하십시오.

스택을 사용하여 메모리 영역을 더 빨리 블랭킹하는 방법이있었습니다. LDI와 LDIR의 사용은 매우 일반적 이었지만 David Webb (국경을 포함한 전체 화면 번호 카운트 다운과 같은 모든 방식으로 ZX 스펙트럼을 추진 한 사람)는이 기술을 4 배 더 빠르게 만들었습니다.

  • 스택 포인터를 저장 한 다음 화면 끝으로 이동합니다.
  • HL 레지스터 쌍을 0으로로드하고
  • HL을 스택에 밀어 넣는 거대한 루프로 들어갑니다.
  • 스택은 화면 위로 올라가서 메모리를 통해 그리고 프로세스에서 화면을 지 웁니다.

위의 설명은에서 가져 왔습니다 David Webbs Game Starion의 검토.

Z80 루틴은 다음과 같습니다.

  DI              ; disable interrupts which would write to the stack.
  LD HL, 0
  ADD HL, SP      ; save stack pointer
  EX DE, HL       ; in DE register
  LD HL, 0
  LD C, 0x18      ; Screen size in pages
  LD SP, 0x4000   ; End of screen
PAGE_LOOP:
  LD B, 128       ; inner loop iterates 128 times
LOOP:
  PUSH HL         ; effectively *--SP = 0; *--SP = 0;
  DJNZ LOOP       ; loop for 256 bytes
  DEC C
  JP NZ,PAGE_LOOP
  EX DE, HL
  LD SP, HL       ; restore stack pointer
  EI              ; re-enable interrupts

그러나 그 루틴은 두 배나 빠릅니다. LDIR은 21주기마다 1 바이트를 복사합니다. 내부 루프는 24 사이클마다 두 바이트를 복사합니다. PUSH HL 그리고 13 DJNZ LOOP. 거의 4 배나 빠르게 내부 루프를 풀기 위해 :

LOOP:
   PUSH HL
   PUSH HL
   ...
   PUSH HL         ; repeat 128 times
   DEC C
   JP NZ,LOOP

이는 LDIR의 바이트 당 21 사이클보다 약 3.8 배 빠른 두 바이트마다 거의 11 사이클입니다.

의심 할 여지 없이이 기술은 여러 번 재창조되었습니다. 예를 들어, 초기에 나타났습니다 TRS-80의 서브 로그의 비행 시뮬레이터 1 1980 년.

Memmove와 Memcpy가 왜 이런 식으로 행동합니까?

아마도 Z80 하드웨어를 대상으로하는 구체적인 최신 C ++ 컴파일러가 없기 때문에? 하나를 작성하십시오. ;-)

언어는 주어진 하드웨어가 무엇이든 구현하는 방법을 지정하지 않습니다. 이것은 전적으로 컴파일러 및 라이브러리의 프로그래머에 달려 있습니다. 물론 상상할 수있는 모든 하드웨어 구성에 대해 고도로 지정된 버전을 작성하는 것은 많은 작업입니다. 그 이유가 될 것입니다.

이러한 종류의 배열 초기화를 수행하는 합리적인 방법이 있습니까? 이러한 종류의 배열 초기화를 수행하는 합리적인 방법이 있습니까?

글쎄, 다른 모든 것이 실패하면 항상 인라인 어셈블리를 사용할 수 있습니다. 그 외에는 기대합니다 std::fill 좋은 STL 구현에서 가장 잘 수행합니다. 그리고 네, 내 기대가 너무 높고 std::memset 실제로 실제로 더 잘 수행합니다.

당신이 보여주는 Z80 시퀀스는 1978 년에 가장 빠른 방법이었습니다. 그것은 30 년 전이었습니다. 프로세서는 그 이후로 많은 발전을 해왔으며 오늘날에는 가장 느린 방법입니다.

Memmove는 소스와 대상이 겹칠 때 작동하도록 설계되었으므로 메모리 덩어리를 One Byte로 이동할 수 있습니다. 그것은 C 및 C ++ 표준에 의한 지정된 동작의 일부입니다. Memcpy는 지정되지 않습니다. 컴파일러가이를 구현하기로 결정한 방식에 따라 Memmove와 동일하게 작동하거나 다를 수 있습니다. 컴파일러는 Memmove보다 더 효율적인 메소드를 자유롭게 선택할 수 있습니다.

하드웨어 수준에서 피우는 경우 일부 CPU에는 메모리 블록을 매우 빠르게 채울 수있는 DMA 컨트롤러가 있습니다 (CPU가 할 수있는 것보다 훨씬 빠릅니다). Freescale I.MX21 CPU 에서이 작업을 수행했습니다.

이것은 x86 어셈블리에서도 쉽게 달성됩니다. 실제로, 그것은 당신의 예제와 거의 동일한 코드로 요약됩니다.

mov esi, source    ; set esi to be the source
lea edi, [esi + 1] ; set edi to be the source + 1
mov byte [esi], 0  ; initialize the first byte with the "seed"
mov ecx, 100h      ; set ecx to the size of the buffer
rep movsb          ; do the fill

그러나 가능하다면 한 번에 둘 이상의 바이트를 설정하는 것이 더 효율적입니다.

드디어, memcpy/memmove 당신이 찾고있는 것이 아니라, 그것들은 영역에서 다른 지역에서 다른 메모리 블록의 사본을 만드는 것입니다 (Memmove는 소스와 dest가 동일한 버퍼의 일부가 될 수 있습니다). memset 선택한 바이트로 블록을 채 웁니다.

또한 있습니다 Calloc 포인터를 반환하기 전에 메모리를 0으로 할당하고 초기화합니다. 물론 Calloc은 사용자가 지정한 것이 아니라 0으로 만 초기화합니다.

이것이 Z80의 주어진 값으로 메모리 블록을 설정하는 가장 효율적인 방법이라면 가능합니다. memset() Z80을 대상으로하는 컴파일러에서 설명 할 때 구현 될 수 있습니다.

그럴 수도 있습니다 memcpy() 해당 컴파일러에서 유사한 시퀀스를 사용할 수도 있습니다.

그러나 Z80의 완전히 다른 명령 세트를 가진 CPU를 대상으로하는 컴파일러가 왜 이러한 유형의 물건에 Z80 관용구를 사용할 것으로 예상됩니까?

X86 아키텍처에는 비슷한 지침 세트가 있으며, Rep Opcode와 접두사를 만들어 메모리 블록을 복사, 채우기 또는 비교하는 것과 같은 작업을 반복적으로 실행하도록 할 수 있습니다. 그러나 인텔이 386 (또는 아마도 486 일 때)과 함께 나왔을 때 CPU는 실제로 루프의 더 간단한 지침보다 더 느린 지침을 실행할 것입니다. 따라서 컴파일러는 종종 담당자 지침 사용을 중단했습니다.

진지하게, C/C ++를 작성하는 경우 간단한 루프를 작성하고 컴파일러가 귀찮게하십시오. 예를 들어, 다음은이 정확한 경우에 생성 된 일부 코드 vs2005 (템플릿 크기 사용)입니다.

template <int S>
class A
{
  char s_[S];
public:
  A()
  {
    for(int i = 0; i < S; ++i)
    {
      s_[i] = 'A';
    }
  }
  int MaxLength() const
  {
    return S;
  }
};

extern void useA(A<5> &a, int n); // fool the optimizer into generating any code at all

void test()
{
  A<5> a5;
  useA(a5, a5.MaxLength());
}

어셈블러 출력은 다음과 같습니다.

test PROC

[snip]

; 25   :    A<5> a5;

mov eax, 41414141H              ;"AAAA"
mov DWORD PTR a5[esp+40], eax
mov BYTE PTR a5[esp+44], al

; 26   :    useA(a5, a5.MaxLength());

lea eax, DWORD PTR a5[esp+40]
push    5               ; MaxLength()
push    eax
call    useA

그렇습니다 ~ 아니다 그보다 더 효율적으로 얻으십시오. 걱정을 멈추고 컴파일러를 신뢰하거나 최소한 최적화 방법을 찾기 전에 컴파일러가 생성하는 내용을 살펴보십시오. 비교를 위해 코드를 사용하여 컴파일했습니다 std::fill(s_, s_ + S, 'A') 그리고 std::memset(s_, 'A', S) for-loop 대신에 컴파일러가 동일한 출력을 생성했습니다.

PowerPC에있는 경우 _DCBZ ().

정의 된 동작이 모든 일에 걸쳐 메모리 범위의 시작 부분을 복사하는 "Memspread"기능을 갖는 것이 유용한 여러 상황이 있습니다. Memset ()은 단일 바이트 값을 확산시키는 것이 목표로서 잘 수행되지만, 예를 들어 정수 배열을 동일한 값으로 채울 수있는 경우가 있습니다. 많은 프로세서 구현에서 소스에서 대상으로 한 번에 바이트를 복사하는 것은이를 구현하는 매우 어리석은 방법이지만 잘 설계된 기능은 좋은 결과를 얻을 수 있습니다. 예를 들어, 데이터 양이 32 바이트 미만인지 확인하여 시작하십시오. 그렇다면 바이트 복사본 만하십시오. 그렇지 않으면 소스 및 대상 정렬을 확인하십시오. 그들이 정렬되면 크기를 가장 가까운 단어 (필요한 경우)로 내려 놓고 어디에나있는 첫 번째 단어를 복사하고, 어디에나 다음 단어를 복사합니다.

나도 때때로 상향식 memcpy로 작동하도록 지정된 함수를 원했습니다. 예정된 겹치는 범위와 함께 사용합니다. 왜 표준이 없는지에 관해서는 아무도 그것이 중요하다고 생각하지 않았다고 생각합니다.

memcpy() 그 행동이 있어야합니다. memmove() 메모리 블록이 겹치는 경우 디자인으로는 버퍼 끝에서 시작하는 내용을 복사하여 이러한 종류의 동작을 피합니다. 그러나 특정 값으로 버퍼를 채우려면 사용해야합니다. memset() C 또는 std::fill() 대부분의 최신 컴파일러가 적절한 블록 채우기 명령어 (예 : X86 아키텍처의 Rep STOSB)에 최적화 할 C ++에서.

앞에서 말했듯이 Memset ()은 원하는 기능을 제공합니다.

Memcpy ()는 소스 및 대상 버퍼가 겹치지 않는 경우 또는 Dest <소스의 모든 경우에 메모리 블록 주위를 이동하기위한 것입니다.

Memmove ()는 버퍼가 겹치는 경우 및 dest> 소스의 경우를 해결합니다.

X86 아키텍처에서 우수한 컴파일러는 Memset Call을 인라인 어셈블리 지침으로 직접 대체하여 대상 버퍼의 메모리를 매우 효과적으로 설정하고, 4 바이트 값을 사용하여 가능한 한 오랫동안 채우는 것과 같은 추가 최적화를 적용 하더 x86 어셈블리 코드를 오랫동안 사용하지 않음) :

lea edi,dest
;copy the fill byte to all 4 bytes of eax
mov al,fill
mov ah,al
mov dx,ax
shl eax,16
mov ax,dx
mov ecx,count
mov edx,ecx
shr ecx,2
cld
rep stosd
test edx,2
jz moveByte
stosw
moveByte:
test edx,1
jz fillDone
stosb
fillDone:

실제로이 코드는 메모리에 메모리를 수행하지 않고 메모리 이동에만 등록하기 때문에 Z80 버전보다 훨씬 효율적입니다. Z80 코드는 실제로 후속 사본의 소스를 채워진 각 사본 작업에 의존하기 때문에 상당히 해킹입니다.

컴파일러가 반쯤 좋은 경우, MEMSET으로 분해 될 수있는 더 복잡한 C ++ 코드를 감지 할 수 있지만 (아래 게시물 참조), 이것이 중첩 루프에서 실제로 발생하는지 의심 스럽다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top