본 내용는 어언 2년 전 2학년 때 논리회로 과목에서 공부했던 내용과 거의 일치합니다

수업 들을때 이렇게 공부했으면 B+는 받았을텐데.. (B- 받고 재수강 엄두가 안나서 안고 갈 예정 ㅠㅠ)

아무튼 컴퓨터, 혹은 컴퓨터와 비슷하게 논리회로를 사용하는 곳에서 음수를 표현하는 방법에는 3가지가 있습니다!!

그 방식들에 대해서 알아보도록 하죠!

 

목차

     


    0. 부호 절대값 방식, 그리고 보수!

    결론부터 말하면 보수의 개념컴퓨터가 음수를 표현하기 위해 만들어졌습니다. (이게 제일 중요합니다!!!!)

     

    우리가 알다시피 컴퓨터는 멍청하기 때문에(물론 제가 더 멍청하지만..) 0과 1밖에 모릅니다.

    그래서 -라는 기호를 쓸 수가 없습니다.

     

    간단하게 0000이라는 4bit짜리 메모리공간의 맨 앞자리를

    0이면 +, 1이면 -라고 하면 되지 않냐고 생각할 수 있습니다.(부호 절대값 방식)

    ▶ 부호 절대값 방식

    0000 +0 1000 -0

    0001 +1 1001 -1

    0010 +2 1010 -2

    0011 +3 1011 -3

    0100 +4 1100 -4

    0101 +5 1101 -5

    0110 +6 1110 -6

    0111 +7 1111 -7

    만약 1+(-1)=0을 연산하기 위해서 위에 나와있는대로 계산해보면

    0001(1) + 1001(-1) = 0000(0) 이 나와야겠죠?

    그런데 직접 계산해보면 1010(-2)가 나옵니다.

    이렇게 -를 포함한 수를 연산할 때 생기는 각종 문제를 해결하기 위해서 보수연산법이 등장했습니다!!!

     


    1. 보수? 그게 뭔데??

    위에서 말했다시피, 컴퓨터가 음수를 표현하기 위해 사용하는 것보수입니다.

    우선 2진수의 2의 보수와 1의 보수를 설명하기 전에

    이해를 돕기 위해 우리가 익숙한 10진수로 예를 들어보겠습니다!!

    1.0. 10진수 case

    10진수에는 10의 보수9의 보수가 있습니다.

    10의 보수숫자의 합을 10의 제곱수로 만들어 주기 위해 더해줘야 하는 수

    - 각 자릿수의 값을 9에서 빼버리고 1을 더하여 얻을 수 있는 보수

     

    9의 보수숫자의 합을 10의 제곱수-1로 만들어주기 위해 더해줘야 하는 수

    - 각 자릿수의 값을 9에서 빼버리고 얻을 수 있는 보수

    4의 10의 보수는 6 → 9 - 4 + 1 = 6

    4의 9의 보수는 5 → 9 - 4 = 5

    14의 10의 보수는 86 → 99 - 14 + 1 = 86

    14의 9의 보수는 85 → 99 - 14 = 85

    1.1. 2진수 case

    이걸 2진수에 그대로 대입해 보자구요!

    2의 보수숫자의 합을 2의 제곱수로 만들어 주기 위해 더해줘야 하는 수

    - 각 자릿수의 값을 1에서 빼버리고 1을 더하여 얻을 수 있는 보수

     

    1의 보수숫자의 합을 2의 제곱수-1로 만들어주기 위해 더해줘야 하는 수

    - 각 자릿수의 값을 1에서 빼버리고 얻을 수 있는 보수 (간단하게 말하면 그냥 다 뒤집으면 됩니다!!)

    1010의 2의 보수는 0110 → 1111 - 1010 + 1 = 0110

    1010의 1의 보수는 0101 → 1111 - 1010 = 0101

    10101010의 2의 보수는 01010110 → 11111111 - 10101010 + 1 = 01010110

    10101010의 1의 보수는 01010101 → 11111111 - 10101010 = 01010101

    딱 보면 뭔가 보이는 것 같은데요..?

    2의 보수는 1의 보수를 취하고 + 1을 하면 되겠죠!!

    (이건 9의 보수와 10의 보수 사이의 관계에서도 성립합니다!)

    ▶ 1의 보수

    0000 +0 1000 -7

    0001 +1 1001 -6

    0010 +2 1010 -5

    0011 +3 1011 -4

    0100 +4 1100 -3

    0101 +5 1101 -2

    0110 +6 1110 -1

    0111 +7 1111 -0

    ▶ 2의 보수 (0이 1개뿐이라 +를 생략)

    0000 0 1000 -8

    0001 1 1001 -7

    0010 2 1010 -6

    0011 3 1011 -5

    0100 4 1100 -4

    0101 5 1101 -3

    0110 6 1110 -2

    0111 7 1111 -1

    ex) 0001(1)에다가 2의 보수를 때려버리는 경우를 생각해보면,

    0001 반전시키면 1110, 여기다가 1을 더하면 1111

    이게 바로 -1이 됩니다!


    2. 실전 보수 테스트!!

    이제 실제로 연산을 해봐야겠죠?

    순서를 반대로 해서 먼저 9의 보수로 연산해보겠습니다.

     

    6 + (-6)을 하기 위해 6의 9의 보수인 3을 사용하면 6+3 = 9.. ?

    0이 아닌데? 라고 생각한다면 여기서 나온 9에 9의 보수를 한번 더 사용하면 0이 잘 출력됩니다.

    좀 어렵지만, 조금만 참고 따라가보죠!!

    10의 보수를 사용해서 계산해보면

    6의 10의 보수는 4이고, 보수를 이용해 6 + (-6)을 계산해보면

    6 + 4 = 10, 그런데 1bit짜리 메모리에는 10이라는 2bit짜리 데이터가 들어갈 수 없는데

    (10진수는 실제로는 1bit가 아니지만 편의를 위해서!!!)

    10의 보수에서는 이렇게 데이터 표현범위를 초과(overflow)한 자료는 무시하게 됩니다.

    그래서 맨 앞에 1 지우면 0. 깔끔하죠!!!

    이제 같은 방법으로 1의 보수를 이용해서 계산해보면,

    1010의 1의 보수는 간단히 뒤집은 0101입니다.

    1010+0101하면 1111이다. 근데 0이 아니잖아요?! (사실 0은 맞는데, -0입니다 ㅋㅋㅋ)

    +0을 만들기 위해서 한번 더 1의 보수를 취 0000이 잘 나옵니다.

    이번엔 2의 보수를 이용해서 계산해보겠습니다.

    1010의 2의 보수는 0101에 1을 더한 0110이다.

    1010+0110하면 1 0000 입니다.

     

    이건 당연한 일이죠? 위에서 1010+0101이 1111인데 여기다 1 더하면 1 0000 이니까요!!

    (시각적 편의를 위해 4bit 단위로 띄어서 썼습니다.)

     

    근데 10의 보수와 동일하게, 2의 보수에서도 데이터 표현범위를 초과(overflow)한 자료는 버려집니다.

    4bit짜리 메모리에 5bit 자료가 들어갈 수는 없잖아요..

    그렇게 맨 앞에 1을 버리면 우리가 원하던 값인 0000이 도출됩니다!

    ++ 여기서 1의 보수 혹은 9의 보수로 만든 음수를 이용해 덧셈 계산을 할 때

    한자리가 더 길어질 수도 있는데(이걸 캐리값이라고 부릅니다),

    이 경우에는 결과값에 캐리값을 더하면 됩니다.

    ex) 5 + (-3) = 0101 + 1100 = 1 0001 = 0001 + 1 = 0010 = 2

     


    3. ~ 비트연산자 실전 계산!!!

    이쯤에서 보수에 대한 이야기는 그만 하고 실전 비트연산자를 계산해보도록 하죠.

    우선 ~라는 비트연산자는 모든 비트를 뒤집는 연산을 실행해주는 비트연산자입니다.

     

    3.0. ~6

    5bit짜리 메모리에 담겨있는 6을 예로 들어보면,

    6을 2진수로 표현하면 0 0110입니다.

     

    그런데, 파이썬의 int는 signed 자료형으로,

    맨 앞에 붙는 비트가 MSB(Most Significant Bit)라고 해서 부호를 나타냅니다.

    (여담으로 부호를 나타내는 비트기 때문에 sign bit라고도 부르기도 합니다!!)

     

    여기서 MSB는 0이면 +, 1이면 -를 나타내는데

    우리가 예로 든 6은 양수이므로

    0 0110이라고 볼 수 있다.

    (실제로 이런 5bit짜리 메모리는 보기 힘들지만.. MSB를 따로 두면 좀 더 이해가 편할 것 같아서 이렇게 예를 들었습니다!)

    이제 ~6을 구하기 위해 비트를 전부 뒤집어줍니다.

    그럼 1 1001이 나오죠.

    바로 이게 컴퓨터가 인식하는 ~6의 값입니다.

    맨 앞에 있는 MSB가 1이니까 음수겠죠!!!

    위에서 말했다시피 연산을 위해서

    컴퓨터는 음수를 자신만의 방식(2의 보수)으로 저장합니다.

    그래서 우리가 익숙한 2진법으로 컴퓨터에 저장된 내용을 해독하기 위해서는

    2의 보수를 취해준 것을 역으로 풀어줘야 합니다.

     

    다시 말하면 MSB가 1인 경우 (음수인 경우)

    2의 보수를 반대로 사용해야 우리가 아는 2진수 값이 도출됩니다.

    2의 보수를 취하는 방법은 1의 보수를 취하고 1을 더해주는 것이기 때문에

    이걸 반대로 사용할 때는 1을 빼주고 1의 보수를 취하면 됩니다.

    아무튼 1 1001에서 1을 빼줍니다.

    그럼 1 1000인데 여기서 1의 보수를 취하면 0 0111이 나옵니다!

     

    이건 7인데, 원래 앞에 부호가 1이 붙었으니까 -7이 나옵니다.

    즉 ~6 = -7이라고 볼 수 있는거죠!!!

    3.1. ~(-4)

    이번엔 -4를 ~연산자로 뒤집어보려고 합니다. (~-4)

    우선 컴퓨터가 인식하는 -4를 구하기 위해서

    0 0100(4)에 해당하는 2의 보수를 구하면

    1 1011에다가 1 더하면 1 1100,

    즉 컴퓨터는 우리가 아는 -4를 1 1100로 변환해서 메모리에 저장해 놓을 것이라고 생각할 수 있습니다.

    반대로 이제 이 1 1100을 우리에게 익숙한 10진법으로 표현할 때는

    1을 빼주고 1의 보수를 취하면 되겠죠? (아까 1의 보수 취하고 1 더했으니까 반대로!!)

     

    인간의 언어로 1 1100을 번역하기 위해서 다시 역으로 연산하는 작업을 해보겠습니다.

    1 1100에서 1 빼면 1 1011, 1 1011에서 1의 보수를 취하면 0 0100,

    해독할 때 보수는 MSB(sign bit)를 건드리지 않으므로 (원래 앞에 부호가 1이었으니까)

    우리가 보기 편한 2진법으로는 1 0100, -4를 출력하게 되는 것이죠.

    아무튼 이 -4를 2진법으로 구했으니, 뒤집어보겠습니다.

    컴퓨터가 알고있는 1 1100 (우리가 아는 -4)에 ~를 적용하면 0 0011이 됩니다.

    근데 이건 MSB(sign bit)가 0이기 때문에 따로 보수를 취해서 음수를 구할 필요가 없습니다!! (편하네요 ㅎㅎ)

    고로 3이 출력됩니다. 간단하죠!!!!

    3.2. ~3

    이번에는 4bit짜리 메모리를 가진 3을 뒤집어보겠습니다. (~3)

    0011에다가 먼저 ~를 계산하기 위해 전체 비트를 뒤집으면 1100입니다.

    컴퓨터가 인식하는 1100은 MSB가 1인 음수이므로

    우리가 아는 10진법으로 볼 수 있게 하려면 2의 보수를 반대로 취해줘야합니다.

    1100 - 1을 하면 1011이 되고 이제 이걸 1의 보수를 취하면 0100이 됩니다.

    이제 원래 있던 -를 다시 붙여주면 -4가 됩니다.

     


    4. 진짜 결론​

    파이썬 위키 공식문서에는 이렇게 나와있습니다.

    Returns the complement of x - the number you get by switching each 1 for a 0 and each 0 for a 1. This is the same as -x -1

    그러니까 한마디로 ~뒤에 숫자 x를 넣으면 -(x+1) 이 된다고 공식처럼 생각하면 됩니다.

    -4 넣으면 3, 2 넣으면 -3 이렇게!!

    이 원리를 이해하려고 이렇게 더럽고 복잡하고 치졸하게 쓴 것이죠.. 후...

     

    BitwiseOperators - Python Wiki

    FAQ: What do the operators <<, >>, &, |, ~, and ^ do? These are Python's bitwise operators. Preamble: Twos-Complement Numbers All of these operators share something in common -- they are "bitwise" operators. That is, they operate on numbers (normally), but instead of treating that number as if it w...

    wiki.python.org

     

    4.0. 보수연산 ++

    이번엔 8bit 메모리를 차지하는 극단적인 예를 들어보려고 합니다.

    컴퓨터가 아는 1000 0000은 과연 뭘까요?

    일단 signed 자료형이라고 가정하면 MSB는 1이기 때문에 2의 보수가 적용되어 있을 것이라고 추측할 수 있습니다.

    그리고 MSB가 1이니까 음수라는 정보를 얻을 수 있습니다.

    이제 저 암호를 해독해보면

    먼저 1을 빼고 (0111 1111)

    1의 보수를 취하면 (1000 0000) 128이 나오고,

    원래 MSB가 1이니까 -128이라는 것을 알 수 있습니다.

    (sign값을 가진 8bit자료는 -128~127까지의 데이터를 담을 수 있습니다!!)

     

    4.1. 2의 보수의 효율성!

    이쯤 되면 눈치 챘을 수도 있는데, 여기서 2의 보수를 사용하는 이유가 하나 드러납니다.

    0000(+0)의 1의 보수는 1111(-0)입니다.

    하지만 0000(+0)의 2의 보수는?

    0000에 1의 보수를 취하면 1111, 여기에 1 더하면 1 0000

    근데 2의 보수에서 발생하는 overflow는 버려집니다.

    따라서 0000의 2의 보수는 0000입니다.

     

    어..? 1의 보수를 사용할 때 있었던 -0의 자리가 사라졌네요??

    -0이 사라지고, 대신 한칸씩 땡겨서 -8이 들어가게 된 것이죠!!

    다시 말하면 어짜피 +0과 -0은 같은 수이니 숫자 하나를 더 표기할 수 있게 된 것입니다!

    같은 메모리인데 숫자 1개를 더 표시할 수 있다? 개이득이라는 말이겠죠!

     

    다시 보는 1의 보수

    0000 +0 1000 -7

    0001 +1 1001 -6

    0010 +2 1010 -5

    0011 +3 1011 -4

    0100 +4 1100 -3

    0101 +5 1101 -2

    0110 +6 1110 -1

    0111 +7 1111 -0

    다시 보는 2의 보수 (0이 1개뿐이라 +를 생략)

    0000 0 1000 -8

    0001 1 1001 -7

    0010 2 1010 -6

    0011 3 1011 -5

    0100 4 1100 -4

    0101 5 1101 -3

    0110 6 1110 -2

    0111 7 1111 -1

     


    5. Q&A 및 잡담

    5.0. 1의 보수가 훨씬 간편한데 왜 2의 보수를 쓸까?

    가장 큰 이유는 0이 2개나 존재하는 모순 때문입니다.

    +0과 -0이 꼬일걸 생각해보면.. 어우 머리아퍼라...

     

    그리고 위의 연산부분에서 했던걸 다시 찾아보면 0000을 만들어주기 위해 1의 보수는

    4bit 환경에서 8번, 8bit 환경에서 16번 비트를 뒤집어야합니다.

    즉 비트를 2n번 만큼 뒤집어야 하죠.

    (보수를 만들어주려고 한번 다 뒤집고, 마지막에 1111을 0000으로 만들어주기 위해서 전체 비트를 한번 더 뒤집었습니다!!)

     

    하지만 2의 보수는 4bit 환경에서 4번 뒤집고 +1, 8bit 환경에서 8번 뒤집고 +1,

    즉 비트를 n + 1 (자리수 넘어가면 2)번만 뒤집어도 되기 때문에 일을 덜해도 됩니다!

     

    또한 계산 과정중 발생한 캐리를 따로 더해주는 1의 보수보다는

    그냥 버리는 2의 보수쪽이 회로를 짜는 입장에서는 훨씬 간편하겠죠?!!

    그래서 대다수의 회로는 2의 보수를 사용하게 되었다고 합니다.

    ​5.1. 파이썬 외 다른 언어에서도 동일한가?

    저도 코린이라 모든 언어를 다 알 수는 없지만, 아마 동일한 메커니즘일 것이라고 추측합니다.

    여러분이 만약 다른 언어 (C++, C++, C++ 등...)를 배우게 된다면

    unsigned와 signed 자료형을 나눠서 사용하게 될 일이 있을 지도 모릅니다.

    (위 내용을 이미 이해했다면 껌(?)입니다)

    부호가 없는 unsigned같은 경우에는 냅다 뒤집으면 됩니다. (1의 보수)

     

    7의 예를 들어보죠!

    ~7은 111을 전부 거꾸로 하면 되니까 000, 즉 0입니다.

    13의 예를 들어보면

    ~13은 1101을 전부 거꾸로 하면 되니까 0010, 즉 2입니다.

    이건 주어진 bit에 맞춰서 뒤집기만 하면 되니 이해하기도 쉽습니다!

     

    C++에서 unsigned char a = 4; 를 뒤집으면 (8bit)

    0000 0100 을 뒤집으면 되니까 1111 1011이니까 251이 됩니다.

    이런식으로 unsigned 자료형에서는 음수가 없기 때문에

    써야하는 곳에 잘 확인해서 쓰면 됩니다!

     

    복습하자면!!!

    4bit signed 자료형은 2의 보수 사용시 (2^3-1) ~ -(2^3) 까지 표현 가능하고

    4bit unsigned 자료형은 2의 보수 사용시 0~(2^4-1) 까지 표현 가능합니다!

    5.2. 중간에 컴퓨터에 저장된 2진수를 불러올때 왜 따로 -를 붙일까?

    컴퓨터에 저장된 MSB가 1인 2진수를 우리가 아는 2진수로 불러올 때

    2의 보수를 거꾸로 취해서 불러오는데

    여기서 우리가 도출한 수는 양수일 것이기 때문에

    (당연한 것이, 컴퓨터가 양수에 -기호 붙이려고 보수작업을 하는 것이니, 보수작업을 반대로 하면 원래 양수가 나오겠죠?)

    원래 컴퓨터가 가진 자료는 음수이기 때문에 -를 따로 붙여주는 것입니다!

     


    이 내용은 같은 동아리의 지호라는 친구가 python 수업을 듣는 중 궁금해 하는 점에 대해 답변해 주기 위해서

    직접 긴 시간 동안 생각하고 찾아가며 최대한 쉽게 풀어쓰려고 동아리 내 notion과 이전에 사용했던 네이버 블로그에 작성했던 내용입니다!

     

    직접 읽어보니 가독성이 그다지 좋지는 못하네요 ㅠ

    궁금한 부분이 있으시거나 오류가 있다면 주저 없이 댓글 남겨주세요!

     

    반응형
    • 네이버 블로그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기