나는 더프의 장치에서 Wikipedia 의 기사를 읽었으며 그것을 얻지 못했습니다. 정말 관심이 있지만 거기에 대한 설명을 몇 번 읽었지만 여전히 더프의 장치 작동 방식을 알 수 없습니다.
더 자세한 설명은 무엇입니까?
나는 더프의 장치에서 Wikipedia 의 기사를 읽었으며 그것을 얻지 못했습니다. 정말 관심이 있지만 거기에 대한 설명을 몇 번 읽었지만 여전히 더프의 장치 작동 방식을 알 수 없습니다.
더 자세한 설명은 무엇입니까?
답변:
다른 곳에서 좋은 설명이 있지만 시도해 보겠습니다. (이것은 화이트 보드에서 훨씬 쉽습니다!) 다음은 몇 가지 표기법이있는 Wikipedia 예입니다.
20 바이트를 복사한다고 가정 해 봅시다. 첫 번째 패스에 대한 프로그램의 흐름 제어는 다음과 같습니다.
int count; // Set to 20
{
int n = (count + 7) / 8; // n is now 3. (The "while" is going
// to be run three times.)
switch (count % 8) { // The remainder is 4 (20 modulo 8) so
// jump to the case 4
case 0: // [skipped]
do { // [skipped]
*to = *from++; // [skipped]
case 7: *to = *from++; // [skipped]
case 6: *to = *from++; // [skipped]
case 5: *to = *from++; // [skipped]
case 4: *to = *from++; // Start here. Copy 1 byte (total 1)
case 3: *to = *from++; // Copy 1 byte (total 2)
case 2: *to = *from++; // Copy 1 byte (total 3)
case 1: *to = *from++; // Copy 1 byte (total 4)
} while (--n > 0); // N = 3 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it is)
}
이제 두 번째 패스를 시작하면 표시된 코드 만 실행합니다.
int count; //
{
int n = (count + 7) / 8; //
//
switch (count % 8) { //
//
case 0: //
do { // The while jumps to here.
*to = *from++; // Copy 1 byte (total 5)
case 7: *to = *from++; // Copy 1 byte (total 6)
case 6: *to = *from++; // Copy 1 byte (total 7)
case 5: *to = *from++; // Copy 1 byte (total 8)
case 4: *to = *from++; // Copy 1 byte (total 9)
case 3: *to = *from++; // Copy 1 byte (total 10)
case 2: *to = *from++; // Copy 1 byte (total 11)
case 1: *to = *from++; // Copy 1 byte (total 12)
} while (--n > 0); // N = 2 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it is)
}
이제 세 번째 단계를 시작하십시오.
int count; //
{
int n = (count + 7) / 8; //
//
switch (count % 8) { //
//
case 0: //
do { // The while jumps to here.
*to = *from++; // Copy 1 byte (total 13)
case 7: *to = *from++; // Copy 1 byte (total 14)
case 6: *to = *from++; // Copy 1 byte (total 15)
case 5: *to = *from++; // Copy 1 byte (total 16)
case 4: *to = *from++; // Copy 1 byte (total 17)
case 3: *to = *from++; // Copy 1 byte (total 18)
case 2: *to = *from++; // Copy 1 byte (total 19)
case 1: *to = *from++; // Copy 1 byte (total 20)
} while (--n > 0); // N = 1 Reduce N by 1, then jump up
// to the "do" if it's still
} // greater than 0 (and it's not, so bail)
} // continue here...
이제 20 바이트가 복사됩니다.
참고 : 원래의 더프 장치 (위 그림 참조)는 to
주소 의 I / O 장치로 복사되었습니다 . 따라서 포인터를 증가시킬 필요가 없습니다 *to
. 두 개의 메모리 버퍼 사이를 복사 할 때는을 사용해야 *to++
합니다.
do
많이 보지 마십시오 . 대신 봐 switch
와 while
구식 계산으로 GOTO
문에서도 나 어셈블러 jmp
오프셋과 문. 은 switch
다음 몇 가지 수학을 수행하고 jmp
적절한 장소에이야. 은 while
부울 검사를 수행하고 맹목적으로 jmp
(가) 위치에 대한 오른쪽이야 do
이었다.
Dr. Dobb 's Journal 의 설명은 이 주제에서 찾은 최고입니다.
이것은 나의 AHA 순간입니다.
for (i = 0; i < len; ++i) {
HAL_IO_PORT = *pSource++;
}
된다 :
int n = len / 8;
for (i = 0; i < n; ++i) {
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
HAL_IO_PORT = *pSource++;
}
n = len % 8;
for (i = 0; i < n; ++i) {
HAL_IO_PORT = *pSource++;
}
된다 :
int n = (len + 8 - 1) / 8;
switch (len % 8) {
case 0: do { HAL_IO_PORT = *pSource++;
case 7: HAL_IO_PORT = *pSource++;
case 6: HAL_IO_PORT = *pSource++;
case 5: HAL_IO_PORT = *pSource++;
case 4: HAL_IO_PORT = *pSource++;
case 3: HAL_IO_PORT = *pSource++;
case 2: HAL_IO_PORT = *pSource++;
case 1: HAL_IO_PORT = *pSource++;
} while (--n > 0);
}
len%8
4 인 경우에도 사례 4, 사례 2, 사례 2 및 사례 1을 실행 한 후 다음 루프부터 모든 사례 를 건너 뛰어 실행 합니다 . 이것은 루프와 스위치 문장이 "상호 작용하는"방식을 설명해야하는 부분입니다.
len % 8
바이트가 복사되지 않습니까?
더프의 장치에는 두 가지 중요한 사항이 있습니다. 첫째, 내가 이해하기 쉬운 부분이라고 생각되는 루프는 풀립니다. 이것은 루프의 완료 여부를 확인하고 루프의 맨 위로 점프하는 것과 관련된 오버 헤드를 피함으로써 더 큰 코드 크기를 더 빠른 속도로 교환합니다. CPU는 점프 대신 직선 코드를 실행할 때 더 빠르게 실행될 수 있습니다.
두 번째 측면은 switch 문입니다. 코드가 처음으로 루프의 중간 으로 이동합니다. 대부분의 사람들에게 놀라운 부분은 그러한 것이 허용된다는 것입니다. 글쎄요. 실행은 계산 된 사례 레이블에서 시작한 다음 다른 스위치 문과 마찬가지로 각 연속 할당 문으로 넘어갑니다 . 마지막 사례 레이블 이후에, 실행은 루프의 맨 아래에 도달하며,이 시점에서 다시 맨 위로 이동합니다. 루프의 상단은 switch 문 안에 있으므로 스위치는 더 이상 재평가되지 않습니다.
원래 루프는 8 번 풀리므로 반복 횟수는 8로 나뉩니다. 복사 할 바이트 수가 8의 배수가 아닌 경우 남은 바이트가 있습니다. 한 번에 바이트 블록을 복사하는 대부분의 알고리즘은 마지막에 나머지 바이트를 처리하지만 Duff의 장치는 처음에 처리합니다. 이 함수 count % 8
는 switch 문을 계산 하여 나머지 내용을 파악하고 해당 바이트 수의 사례 레이블로 이동하여 복사합니다. 그런 다음 루프는 8 바이트 그룹을 계속 복사합니다.
더프 장치의 요점은 엄격한 memcpy 구현에서 수행되는 비교 횟수를 줄이는 것입니다.
'count'바이트를 a에서 b로 복사하려고한다고 가정하면 간단한 방법은 다음을 수행하는 것입니다.
do {
*a = *b++;
} while (--count > 0);
카운트가 0보다 큰지 몇 번 비교해야합니까? '횟수'번.
이제 더프 장치는 의도하지 않은 스위치 케이스의 부작용을 사용하여 카운트 / 8에 필요한 비교 횟수를 줄일 수 있습니다.
이제 더프 장치를 사용하여 20 바이트를 복사한다고 가정하면 얼마나 많은 비교가 필요합니까? 4 개만 복사하는 마지막 첫 번째 바이트를 제외하고 한 번에 8 바이트를 복사하므로 3 개만 .
업데이트 : 8 개의 비교 / case-in-switch 문을 수행 할 필요는 없지만 기능 크기와 속도 사이의 균형은 합리적입니다.
처음으로 읽을 때 자동 서식을 지정했습니다.
void dsend(char* to, char* from, count) {
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do {
*to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
}
무슨 일이 있었는지 전혀 몰랐습니다.
이 질문을 받았을 때가 아니라 지금 Wikipedia에 대한 설명이 아주 좋습니다.
이 장치는 C의 두 가지 속성으로 인해 유효하고 합법적입니다.
- 언어 정의에서 switch 문을 편안하게 지정합니다. 장치가 발명 될 당시 이것은 C 프로그래밍 언어의 첫 번째 판으로, 스위치의 제어 된 문장 만이 구문 상 유효한 (복합) 문장이어야하며,이 경우 레이블은 하위 문장 앞에 접두사로 나타날 수 있습니다. break 문이 없으면 제어 흐름이 하나의 케이스 레이블로 제어되는 명령문에서 다음 케이스로 제어되는 명령문으로 넘어 간다는 사실과 함께, 이는 코드가 다음에서 카운트 사본의 연속을 지정 함을 의미합니다. 메모리 소스 출력 포트에 순차적 인 소스 주소.
- C에서 루프 중간으로 합법적으로 점프하는 기능
1 : 더프 장치는 루프 언 롤링의 특별한 의미입니다. 루프 언 롤링이란 무엇입니까?
루프에서 N 번 수행하는 작업이있는 경우 루프를 N / n 번 실행 한 다음 루프 코드에서 n 번 루프를 인라인 (언 롤링)하여 프로그램 크기를 속도로 교환 할 수 있습니다.
for (int i=0; i<N; i++) {
// [The loop code...]
}
와
for (int i=0; i<N/n; i++) {
// [The loop code...]
// [The loop code...]
// [The loop code...]
...
// [The loop code...] // n times!
}
N % n == 0 인 경우 더 잘 작동합니다-더프가 필요하지 않습니다! 그것이 사실이 아니라면 나머지를 처리해야합니다. 이것은 고통입니다.
2 : Duffs 장치는이 표준 루프 언 롤링과 어떻게 다릅니 까?
Duffs 장치는 N % n! = 0 일 때 나머지 루프 사이클을 처리하는 영리한 방법입니다. 전체 do / while은 표준 루프 언 롤링에 따라 N / n 횟수를 실행합니다 (0이 적용되므로). 루프를 통한 마지막 실행 ( 'N / n + 1'시간)에서 케이스가 시작되고 N % n 케이스로 점프하여 루프 코드를 'remainder'횟수만큼 실행합니다.
나는 당신이 요구하는 것을 100 % 확신하지 못하지만, 여기에갑니다 ...
더프의 장치가 다루는 문제는 루프 풀기 중 하나입니다 (당신이 게시 한 위키 링크에서 의심 할 여지가 없습니다). 이것이 기본적으로 동일한 것은 메모리 풋 프린트에 대한 런타임 효율성의 최적화입니다. 더프의 장치는 오래된 문제가 아닌 직렬 복사를 다루지 만 루프에서 비교를 수행해야하는 횟수를 줄여 최적화를 수행하는 방법의 전형적인 예입니다.
이해하기 쉽도록 대체 예제로, 반복하려는 항목 배열이 있고 매번 1을 추가한다고 가정하십시오. 일반적으로 for 루프를 사용하고 약 100 번 반복 할 수 있습니다 . 이것은 상당히 논리적 인 것처럼 보이지만 ...하지만 루프를 풀면 최적화 할 수 있습니다 (분명히 멀지 않습니다 ... 또는 루프를 사용하지 않을 수도 있음).
따라서 규칙적인 for 루프는 다음과 같습니다.
for(int i = 0; i < 100; i++)
{
myArray[i] += 1;
}
된다
for(int i = 0; i < 100; i+10)
{
myArray[i] += 1;
myArray[i+1] += 1;
myArray[i+2] += 1;
myArray[i+3] += 1;
myArray[i+4] += 1;
myArray[i+5] += 1;
myArray[i+6] += 1;
myArray[i+7] += 1;
myArray[i+8] += 1;
myArray[i+9] += 1;
}
더프의 장치는 C 에서이 아이디어를 구현하지만 (위키에서 보았 듯이) 직렬 사본으로 구현합니다. 풀린 예제를 통해 위에서 본 것은 원본의 100과 비교 한 10 개의 비교입니다. 이는 사소하지만 중요한 최적화에 해당합니다.
여기에 내가 더프 장치의 핵심이라고 생각하는 자세한 설명이 있습니다.
문제는 C는 기본적으로 어셈블리 언어의 좋은 외관입니다 (PDP-7 어셈블리는 구체적입니다. 연구를하면 유사성이 얼마나 눈에 띄는지를 알 수 있습니다). 그리고 어셈블리 언어에서는 실제로 루프가 없습니다. 레이블과 조건부 분기 명령이 있습니다. 따라서 루프는 레이블과 분기가있는 전체 명령 시퀀스의 일부일뿐입니다.
instruction
label1: instruction
instruction
instruction
instruction
jump to label1 some condition
스위치 명령은 어느 정도 분기 / 점핑합니다.
evaluate expression into register r
compare r with first case value
branch to first case label if equal
compare r with second case value
branch to second case label if equal
etc....
first_case_label:
instruction
instruction
second_case_label:
instruction
instruction
etc...
조립에서이 두 제어 구조를 결합하는 방법을 쉽게 생각할 수 있으며, 그렇게 생각할 때 C에서의 조합은 더 이상 이상하게 보이지 않습니다.
이것은 내가 Duff 's Device에 관한 다른 질문에 게시 한 답변으로, 질문이 중복으로 종결되기 전에 약간의 격변을 얻었습니다. 이 구성을 피해야하는 이유에 대한 약간의 가치있는 맥락을 제공한다고 생각합니다.
"이것은 더프의 장치 입니다. 루프 반복 횟수가 롤링 계수의 정확한 배수가 아닌 경우 시간을 처리하기 위해 2 차 픽스 업 루프를 추가하지 않아도되는 루프 풀림 방법입니다.
여기에 대부분의 답변이 일반적으로 긍정적 인 것처럼 보이기 때문에 단점을 강조하겠습니다.
이 코드로 컴파일러는 루프 바디에 최적화를 적용하기 위해 고심 할 것입니다. 코드를 간단한 루프로 작성했다면 최신 컴파일러가 언 롤링을 처리 할 수 있어야합니다. 이런 식으로 가독성과 성능을 유지하고 루프 바디에 다른 최적화가 적용되기를 바랍니다.
다른 사람들이 참조한 Wikipedia 기사는이 '패턴'이 Xfree86 소스 코드 성능에서 실제로 제거 된 시점을 말합니다.
이 결과는 필요할 것으로 생각되는 코드를 맹목적으로 최적화하는 것이 일반적입니다. 컴파일러가 작업을 제대로 수행하지 못하게하고 코드를 읽기 어렵고 버그가 발생하기 쉬우 며 일반적으로 속도가 느려집니다. 처음에 올바른 코드를 작성하고 병목 현상을 프로파일 링 한 다음 최적화하는 등 올바른 방법으로 작업을 수행 한 경우에는 이와 같은 것을 사용하지 않을 것입니다. 현대 CPU와 컴파일러가 아닙니다.
이해하는 것이 좋지만, 실제로 사용한다면 놀랄 것입니다. "
실험하면서 스위치와 루프를 인터리빙하지 않고 다른 변형이 발견되었습니다.
int n = (count + 1) / 8;
switch (count % 8)
{
LOOP:
case 0:
if(n-- == 0)
break;
putchar('.');
case 7:
putchar('.');
case 6:
putchar('.');
case 5:
putchar('.');
case 4:
putchar('.');
case 3:
putchar('.');
case 2:
putchar('.');
case 1:
putchar('.');
default:
goto LOOP;
}