+ 연산자가 C에서 구현되는 방식입니까?


80

같은 방법 원시 연산자를 이해하면 +, -, */C에서 구현, 난에서 다음 코드 발견 흥미로운 대답을 .

이 기능은 +실제로 백그라운드에서 작동 하는 방식을 보여주는 것 같습니다 . 그러나 그것을 이해하기에는 너무 혼란 스럽습니다. 이러한 작업은 오랫동안 컴파일러가 생성 한 어셈블리 지시문을 사용하여 수행한다고 믿었습니다!

+운영자 가 게시 된 코드로 구현 되었습니까? 가장 구현? 이것이 2의 보완 또는 기타 구현 종속 기능을 활용합니까?


60
나는 대부분의 구현이 기본 add기계 명령 을 사용 한다고 생각하는데, 모든 CPU가 가지고 있고 몇 클럭에서 작동하는 하드웨어 추가기로 구현 된 것 같습니다.
MikeCAT

3
예, +운영자는 구현 정의 기능을 활용할 가능성이 높습니다. 이를 "머신 언어"및 "CPU"라고합니다. 당신의 질문은 무엇입니까? 표현식이 기계어 코드로 어떻게 변환되는지 알고 싶다면 컴파일러 구성에 대해 읽어보세요.
이 사이트에 대한 너무 정직

11
대부분의 +작업은 기계 코드 명령어 의 일부 변형 (또는 조합) 으로 컴파일됩니다 add. 코드는 모든 실제 시나리오에서 복잡하고 쓸모가 없지만 이진 연산에 대해 가르치는 역할을 할 수 있습니다 .
Anders Tornblad

11
C가 수행하는 방식은 아니지만 (아래 답변 참조) 관련된 회로가 가장 낮은 수준에서 추가 할 수있는 방식과 매우 유사합니다. 작은 이진 값 (예 : 3 또는 4 비트 바이트)에 대해 종이와 펜으로 작업 해보고 어떻게 작동하는지 확인하십시오. 이제 회로가 전기 펄스로 어떻게 똑같은 일을 할 수 있는지 상상해보십시오. 이제 루프가 아닌 병렬로 모든 비트를 수행한다고 상상해보십시오. 이제 1940 년대 컴퓨터를 만들 준비가되었습니다. : D
Jon Hanna

1
추가 명령없이 CPU를 생성 할 수 없다면 어떤 구현에서도 이러한 방식으로 구현되지 않습니다. 게시 한 코드는 CPU가 하드웨어에서 수행하는 작업을 보여줍니다.
user207421

답변:


185

현명하기 위해 C 사양은 방법을 지정하지 않습니다. 추가 구현 .

그러나 현실적으로 + CPU의 단어 크기보다 작거나 같은 정수 유형 연산자는 CPU에 대한 추가 명령으로 직접 변환되고, 더 큰 정수 유형은 오버플로를 처리하기 위해 추가 비트가있는 여러 추가 명령으로 변환됩니다.

CPU는 내부적으로 로직 회로를 사용하여 추가를 구현하고 루프, 비트 시프트 또는 C가 작동하는 방식과 유사한 것을 사용하지 않습니다.


12
이 답변은 비정상적인 명확성과 단순함으로 제공되기 때문에 우수합니다. 나는 그것이 지나치게 현학적 인 것이 아니라 단지 질문에 대한 적절한 양의 pedantry를 찾는 것입니다.
Jeremy Anderson

5
@orlp 실제로 CPU 논리 회로는 HDL에서 컴파일 할 수 있으며 OP의 제안과 모호하게 유사하지만 (모호하게 만) 루프 및 비트 시프트를 사용하여 가산기를 생성 할 가능성이 높습니다. 루프와 비트 시프트는 하드웨어의 레이아웃과 연결 방법을 설명합니다. 그런 다음 최상위 하드웨어에서 누군가가 루프 및 비트 시프트를 풀거나 HDL을 없애고 가산기만큼 중요한 성능을 위해 회로를 수동으로 배치 할 수 있습니다.
Yakk-Adam Nevraumont

5
선형 가산기 회로는 C 코드가하는 일을 정확히 수행하지만 루프는 하드웨어에서 완전히 풀립니다 (32 회).
usr

2
@usr은 단순히 펼쳐진 것이 아니라 모든 "단계"가 동시에 발생합니다.
OrangeDog

4
@OrangeDog 간단한 하드웨어 가산기는 병렬 처리를 제한하는이 C 코드와 매우 유사하게 캐리 리플을 가질 것입니다. 고성능 가산기는이를 줄이기 위해 캐리 룩어 헤드 회로를 사용할 수 있습니다.
plugwash

77

2 비트를 추가하면 다음과 같은 결과가 나타납니다. (진실 표)

따라서 비트 xor를 수행하면 캐리없이 합계를 얻을 수 있습니다. 그리고 비트 단위로 수행하면 캐리 비트를 얻을 수 있습니다.

이 관찰을 다중 비트 수에 대해 확장 a하고b

일단 b이다 0:

따라서 알고리즘은 다음과 같이 요약됩니다.

재귀를 제거하고 루프로 변환하면


위의 알고리즘을 염두에두고 코드에서 설명하는 것이 더 간단해야합니다.

캐리 비트. 두 피연산자의 오른쪽 1 비트가 1 인 경우 캐리 비트는 1입니다.

캐리없는 추가 (캐리 비트 무시 됨)

x를 재사용하여 휴대하도록 설정

더 많은 캐리 비트가있는 동안 반복


재귀 적 구현 (이해하기 쉬움)은 다음과 같습니다.

이 함수는 +가 실제로 백그라운드에서 어떻게 작동하는지 보여줍니다.

아니요. 일반적으로 (거의 항상) 정수 추가는 기계 명령어 추가로 변환됩니다. 이것은 비트 xor 및 and를 사용하는 대체 구현을 보여줍니다.


5
이 IMO 최선의 대답, 그것은 일반적으로 단일 명령어로 변환 있다는 다른 모든 상태이지만,이 것을 수행하고 또한 주어진 기능을 설명합니다.
Nick Sweeting

@NickSweeting 감사합니다. 질문은 두 가지 방식으로 해석 될 수 있으며 수락 된 답변이 OP가 요청한 내용을 올바르게 해석했다고 생각합니다.
Mohit Jain

25

이 함수는 +가 실제로 백그라운드에서 어떻게 작동하는지 보여줍니다.

아니요. 이것은 add실제로 하드웨어 가산기를 사용하는 원시 기계 명령어 로 변환됩니다 .ALU .

컴퓨터가 어떻게 추가되는지 궁금하다면 여기 기본 가산기가 있습니다.

컴퓨터의 모든 것은 대부분 트랜지스터로 만들어진 논리 게이트를 사용하여 수행됩니다. 전체 가산기에는 반 가산기가 있습니다.

논리 게이트 및 가산기에 대한 기본 자습서는 이를 참조 하십시오 . 비디오는 길지만 매우 유용합니다.

이 비디오에는 기본적인 반가산기가 표시됩니다. 간단한 설명이 필요하면 다음과 같습니다.

반 가산기는 2 비트를 더합니다. 가능한 조합은 다음과 같습니다.

  • 0을 더하고 0 = 0
  • 1 더하기 0 = 1
  • 1 더하기 1 = 10 (이진)

이제 반 가산기는 어떻게 작동합니까? 글쎄요, 그것은 세 개의 논리 게이트 and,, xornand. 은 nand 두 입력이 모두 음수이면 양의 전류를 제공하므로 0과 0의 경우를 해결합니다 xor. 입력 중 하나는 양수이고 다른 하나는 음수이므로 양의 출력을 제공하므로 문제가 해결됩니다. 1과 0.and 두 입력이 모두 양수인 경우에만 양의 출력을 제공하므로 1과 1의 문제가 해결됩니다. 기본적으로 이제 절반 가산기를 얻었습니다. 그러나 우리는 여전히 비트를 추가 할 수 있습니다.

이제 우리는 전체 가산기를 만듭니다. 완전 가산기는 반가산기를 반복해서 호출하는 것으로 구성됩니다. 이제 캐리가 있습니다. 1과 1을 더하면 캐리 1이됩니다. 따라서 전체 가산기가하는 일은 반가산기에서 캐리를 가져 와서 저장하고 반가산기에 또 다른 인수로 전달하는 것입니다.

캐리를 어떻게 전달할 수 있는지 혼란 스러우면 기본적으로 먼저 하프 가산기를 사용하여 비트를 더한 다음 합계와 캐리를 더합니다. 이제 2 비트로 캐리를 추가했습니다. 따라서 추가해야 할 비트가 끝날 때까지이 작업을 반복하면 결과를 얻을 수 있습니다.

놀랐나요? 이것이 실제로 일어나는 방법입니다. 긴 프로세스처럼 보이지만 컴퓨터는 나노초의 몇 분의 1 또는 좀 더 구체적으로 말하면 절반의 클록 주기로 처리합니다. 때로는 단일 클록 사이클에서도 수행됩니다. 기본적으로 컴퓨터에는 ALU(의 주요 부분 CPU), 메모리, 버스 등이 있습니다.

논리 게이트, 메모리 및 ALU에서 컴퓨터 하드웨어를 배우고 컴퓨터를 시뮬레이션하려면이 과정을 볼 수 있습니다.이 과정에서 제가이 모든 것을 배웠습니다. 첫 번째 원칙에서 현대적인 컴퓨터 구축

전자 인증서를 원하지 않는 경우 무료입니다. 코스의 2 부는 올해 봄에 예정되어 있습니다.


11
몇 밀리 초? 단일 추가를 위해?
JAB

2
등록 된 값이 두 개인 추가는 일반적으로 단일 시계로 완료됩니다.
Cody Gray

5
@Tamoghna Chowdhury : 나노초의 몇 분의 1을 시도해보십시오. 레지스터 추가는 최신 Intel 프로세서의 IIRC 1 클럭이므로 클럭 속도가 수 GHz입니다. 파이프 라이닝, 슈퍼 스칼라 실행 등은 포함되지 않습니다.
jamesqf

이 잔물결 가산기는 대기 시간을 너무 많이 추가하므로 하드웨어에서 이러한 방식으로 구현되지도 않습니다.
파이프

리플-캐리 가산기는 너무 느리기 때문에 수십 년 동안 CPU에서 사용되지 않았습니다. 대신 단일 클럭 사이클 (또는 인텔의 일부 이중 클럭 ALU의 경우 반 사이클)에서 작업을 수행 할 수있는 더 복잡한 가산기를 사용합니다. (음, 대부분의 CPU는 그것을 사용하지 않습니다. 저가형 임베디드 CPU는 여전히 낮은 트랜지스터 수를 위해 그것을 사용할 수 있습니다.)
Mark

15

C는 추상 기계를 사용하여 C 코드의 기능을 설명합니다. 따라서 작동 방식은 지정되지 않았습니다. 예를 들어 실제로 C를 스크립팅 언어로 컴파일하는 C "컴파일러"가 있습니다.

그러나 대부분의 C 구현에서는 +기계 정수 크기보다 작은 두 정수 사이에서 어셈블리 명령어로 변환됩니다 (여러 단계 후). 어셈블리 명령어는 기계어 코드로 번역되고 실행 파일에 포함됩니다. 어셈블리는 기계 코드에서 "한 단계 제거 된"언어로, 압축 된 바이너리보다 읽기 쉽도록 고안되었습니다.

그런 다음 해당 기계 코드 (여러 단계 후)는 대상 하드웨어 플랫폼에 의해 해석되며, 여기서 CPU의 명령 디코더에 의해 해석됩니다. 이 명령어 디코더는 명령어를 받아 "제어 라인"을 따라 보내는 신호로 변환합니다. 이러한 신호는 레지스터 및 메모리에서 CPU를 통해 데이터를 라우팅하며, 여기서 값은 종종 산술 논리 장치에서 함께 추가됩니다.

산술 논리 장치는 별도의 가산기와 승수를 갖거나 함께 혼합 할 수 있습니다.

산술 논리 장치에는 덧셈 연산을 수행 한 다음 출력을 생성하는 트랜지스터가 많이 있습니다. 상기 출력은 명령 디코더로부터 생성 된 신호를 통해 라우팅되고 메모리 또는 레지스터에 저장된다.

산술 논리 장치와 명령 디코더 모두에있는 상기 트랜지스터의 레이아웃 (그리고 내가 글로 쓴 부품)은 공장의 칩에 새겨 져 있습니다. 에칭 패턴은 종종 하드웨어 설명 언어를 컴파일하여 생성됩니다. 하드웨어 설명 언어는 무엇에 연결되어 있고 어떻게 작동하는지 추상화하고 트랜지스터와 상호 연결 라인을 생성합니다.

하드웨어 설명 언어에는 시간 ( 연속적 으로 )이 아니라 공간에서 일어나는 일 설명하는 시프트 및 루프가 포함될 수 있습니다 . 하드웨어의 다른 부분 간의 연결을 설명합니다. 상기 코드는 위에 게시 한 코드와 매우 유사하게 보일 수 있습니다.

위의 내용은 많은 부품과 레이어에 걸쳐 광택이 있으며 부정확 한 내용이 포함되어 있습니다. 이것은 내 자신의 무능함 (하드웨어와 컴파일러를 모두 작성했지만 둘 다 전문가입니다)과 전체 세부 사항이 SO 게시물이 아닌 경력 한두 번이 필요하기 때문입니다.

다음 은 8 비트 가산기에 대한 SO 게시물입니다. 여기 에 비 SO 게시물이 있습니다. 여기서 일부 가산기 operator+는 HDL에서 사용 됩니다! (HDL 자체 +는 저수준 가산기 코드를 이해 하고 생성합니다.)


14

컴파일 된 C 코드를 실행할 수있는 거의 모든 최신 프로세서에는 정수 추가 기능이 내장되어 있습니다. 게시 한 코드는 정수 추가 연산 코드를 실행하지 않고 정수 추가를 수행하는 영리한 방법이지만 정수 추가가 일반적으로 수행되는 방식은 아닙니다. 실제로 함수 연결은 스택 포인터를 조정하기 위해 어떤 형태의 정수 더하기를 사용합니다.

게시 한 코드는 x와 y를 추가 할 때 공통된 비트와 x 또는 y 중 하나에 고유 한 비트로 분해 할 수 있다는 관찰에 의존합니다.

표현식 x & y(비트 AND)은 x와 y에 공통된 비트를 제공합니다. 표현식 x ^ y(비트 배타적 OR)은 x 또는 y 중 하나에 고유 한 비트를 제공합니다.

합계 x + y는 공통된 비트의 두 배 (x와 y가 모두 해당 비트에 기여하기 때문에)와 x 또는 y에 고유 한 비트의 합계로 다시 쓸 수 있습니다.

(x & y) << 1 공통된 비트의 두 배입니다 (왼쪽 이동에 1이 효과적으로 곱 해짐).

x ^ y x 또는 y 중 하나에 고유 한 비트입니다.

따라서 x를 첫 번째 값으로, y를 두 번째 값으로 바꾸면 합계는 변경되지 않아야합니다. 첫 번째 값은 비트 덧셈의 전달로, 두 번째 값은 비트 덧셈의 하위 비트로 생각할 수 있습니다.

이 프로세스는 x가 0이 될 때까지 계속되며,이 지점에서 y는 합계를 보유합니다.


14

당신이 찾은 코드는 아주 원시적 인 컴퓨터 하드웨어 "add"명령을 어떻게 구현할 있는지 설명하려고합니다 . 방법이 어떤 CPU 에서도 사용되지 않는다는 것을 보장 할 수 있기 때문에 "might"라고 말하고 그 이유를 설명하겠습니다.

정상적인 생활에서는 십진수를 사용하고이를 더하는 방법을 배웠습니다. 두 개의 숫자를 더하려면 가장 낮은 두 자리를 더합니다. 결과가 10 미만이면 결과를 기록하고 다음 자리로 진행합니다. 결과가 10 이상이면 결과를 빼고 10을 적고 ​​다음 숫자로 진행하여 1을 더 추가하는 것을 잊지 마십시오. 예 : 23 + 37, 3 + 7 = 10을 더하고 0을 적고 ​​다음 위치에 1을 더 추가하는 것을 잊지 마십시오. 10 초 위치에 (2 + 3) + 1 = 6을 더하고 적어 둡니다. 결과는 60입니다.

이진수로 똑같은 일을 할 수 있습니다. 차이점은 유일한 자릿수는 0과 1이므로 가능한 합계는 0, 1, 2 뿐이라는 것입니다. 32 비트 숫자의 경우 한 자릿수 위치를 차례로 처리합니다. 이것이 바로 원시 컴퓨터 하드웨어가 그렇게하는 방법입니다.

이 코드는 다르게 작동합니다. 두 자릿수가 1이면 두 이진수의 합이 2라는 것을 알고 있습니다. 두 자릿수가 1이면 다음 이진수 위치에 1을 더하고 0을 기록합니다. 이것이 t의 계산이하는 일입니다. 여기서 두 이진수는 모두 1 (&)이고 다음 자리 위치 (<< 1)로 이동합니다. 그런 다음 더하기를 수행합니다. 0 + 0 = 0, 0 + 1 = 1, 1 + 0 = 1, 1 + 1은 2이지만 0을 적습니다. 이것이 배타적 또는 연산자가하는 일입니다.

그러나 다음 자리에서 처리해야하는 모든 1은 처리되지 않았습니다. 여전히 추가해야합니다. 이것이 코드가 루프를 수행하는 이유입니다. 다음 반복에서 모든 추가 1이 추가됩니다.

프로세서가 그렇게하지 않는 이유는 무엇입니까? 루프이기 때문에 프로세서는 루프를 좋아하지 않으며 느립니다. 최악의 경우 32 회 반복이 필요하기 때문에 속도가 느립니다. 0xffffffff (32 개의 1 비트)에 1을 더하면 첫 번째 반복은 y의 비트 0을 지우고 x를 2로 설정합니다. 두 번째 반복은 비트 1을 지 웁니다. y의 x를 4로 설정합니다. 결과를 얻으려면 32 번의 반복이 필요합니다. 그러나 각 반복은 x 및 y의 모든 비트를 처리해야하므로 많은 하드웨어가 필요합니다.

원시 프로세서는 가장 낮은 위치에서 가장 높은 위치까지 십진수 산술을 수행하는 방식만큼 빠르게 작업을 수행합니다. 또한 32 단계가 필요하지만 각 단계는 이전 비트 위치에서 2 비트와 1 개의 값만 처리하므로 구현하기가 훨씬 쉽습니다. 원시 컴퓨터에서도 루프를 구현하지 않고도이를 수행 할 수 있습니다.

현대적이고 빠르고 복잡한 CPU는 "조건부 합산 기"를 사용합니다. 특히 64 비트 가산기와 같이 비트 수가 많으면 많은 시간을 절약 할 수 있습니다.

64 비트 가산기는 두 부분으로 구성됩니다. 첫째, 가장 낮은 32 비트에 대한 32 비트 가산기입니다. 이 32 비트 가산기는 합계와 "carry"(다음 비트 위치에 1을 추가해야한다는 표시기)를 생성합니다. 둘째, 더 높은 32 비트를위한 두 개의 32 비트 가산기 : 하나는 x + y를 더하고 다른 하나는 x + y + 1을 더합니다. 세 가산기는 모두 병렬로 작동합니다. 그런 다음 첫 번째 가산기가 캐리를 생성하면 CPU는 x + y 또는 x + y + 1 두 결과 중 올바른 결과를 선택하고 완전한 결과를 얻습니다. 따라서 64 비트 가산기는 32 비트 가산기보다 두 배가 아닌 아주 조금만 더 걸립니다.

32 비트 가산기 부분은 여러 16 비트 가산기를 사용하여 조건부 합계 가산기로 다시 구현되고 16 비트 가산기는 조건부 합계 가산기입니다.


13

내 질문은 : + 연산자가 MOST 구현에 게시 된 코드로 구현 되었습니까?

실제 질문에 답해 봅시다. 모든 연산자는 일부 변환 후에 결국 코드로 변환되는 일부 내부 데이터 구조로 컴파일러에 의해 구현됩니다. 실제 컴파일러가 개별 명령문에 대한 코드를 생성하는 경우는 거의 없기 때문에 단일 추가로 생성되는 코드를 말할 수 없습니다.

컴파일러는 실제 작업이 표준에 따라 수행 된 것처럼 동작하는 한 모든 코드를 자유롭게 생성 할 수 있습니다. 그러나 실제로 일어나는 일은 완전히 다를 수 있습니다.

간단한 예 :

여기에서 추가 지침을 생성 할 필요가 없습니다. 컴파일러가 이것을 다음과 같이 번역하는 것은 완벽하게 합법적입니다.

또는 컴파일러는 함수를 foo연속으로 몇 번 호출하고 간단한 산술이며 이에 대한 벡터 명령을 생성한다는 것을 인식 할 수 있습니다. 또는 추가 결과가 나중에 배열 인덱싱에 lea사용되고 명령어가 사용됩니다.

연산자는 거의 단독으로 사용되지 않기 때문에 구현 방법에 대해 말할 수 없습니다.


11

코드 분석이 다른 사람에게 도움이되는 경우 다음 예제를 참조하십시오 x=2, y=6.


x0이 아니므로 y다음에 추가하기 시작하십시오 .

x & y = 2 때문에

2 <<1 = 4<< 1모든 비트를 왼쪽으로 이동 하기 때문입니다 .

요약하면, 그 결과를 숨기고 4에, t

이제 비트 XOR을 적용하십시오 y^=x.

그래서 x=2, y=4. 마지막으로 t+y재설정 x=t하고 while루프 의 시작 부분으로 돌아가서 합계 합니다.

언제 t=0(또는 루프의 시작 부분에서 x=0),


1
캐리 비트를 숨기는 이유에 대한 좋은 설명이 이미 있으므로 코드가 어떻게 작동 하는지 보여 주기 위해이 답변을 게시합니다 .
user1717828 14:52에

11

흥미롭게도 Atmega328P 프로세서에서 avr-g ++ 컴파일러를 사용하는 다음 코드는 -1을 빼서 하나를 더하는 것을 구현합니다.

volatile char x;
int main ()
  {
  x = x + 1;  
  }

생성 된 코드 :

00000090 <main>:
volatile char x;
int main ()
  {
  x = x + 1;  
  90:   80 91 00 01     lds r24, 0x0100
  94:   8f 5f           subi    r24, 0xFF   ; 255
  96:   80 93 00 01     sts 0x0100, r24
  }
  9a:   80 e0           ldi r24, 0x00   ; 0
  9c:   90 e0           ldi r25, 0x00   ; 0
  9e:   08 95           ret

특히 추가는 subi 경우 0xFF가 사실상 -1 인 명령어 (레지스터에서 상수 빼기)에 .

또한 흥미로운 점은이 특정 프로세서에 addi명령어가 없다는 것입니다. 이는 설계자가 보수를 빼는 것이 컴파일러 작성자에 의해 적절하게 처리 될 것이라고 생각했음을 의미합니다.

이것이 2의 보완 또는 기타 구현 종속 기능을 활용합니까?

컴파일러 작성자가 특정 아키텍처에 대해 가능한 가장 효율적인 방법으로 원하는 효과 (한 숫자를 다른 숫자에 추가)를 구현하려고 시도한다고 말하는 것이 타당 할 것입니다. 보수를 빼야한다면 그렇게하세요.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.