밑바닥부터 만드는 컴퓨팅 시스템 진행기

밑바닥부터 만드는 컴퓨팅 시스템 진행기

CS 지식을 쌓기위한 여정…


밑바닥부터 만드는 컴퓨팅 시스템


비전공자로 개발에 입문하면서 CS지식이 빈약했던 나는 CS공부가 참 어렵다고 생각하고 있었다.

실제로 개발할때와 대학에서 배우는 CS 지식간에 괴리감이 있었기 때문이다.

그러니까, KOCW등을 통해 컴공 과목을 수강해봐도, “이거 이렇게 배워서 내가 개발하는데 도움받을 일이 있을까?”

라는 생각이 많이 들었고, 이 부분에서 동기부여가 잘 되지 않았던 것 같다.


아무튼 그런 생각을 항상 갖고 있었는데 넥스트 스텝 과정을 진행할 때 박재성님이 아드님과 진행하셨다는 밑바닥부터 만드는 컴퓨팅 시스템 스터디에 대해 감명 깊게 들었었다.

이 책은 컴퓨터를 직접 만들어보면서 컴퓨터가 어떤 것인지, 어떤 원리로 동작하는지를 이해하는데 목적을 두고있는 책이였다.

즉시 승재님과 이 책으로 스터디를 하기로 모의하고 실행에 옮겼다.

주말 하루에 둘이 만나서 약 7시간 가량의 스터디를 매주 진행해보자는 것이었다.


1장은 불 논리였는데 OR, AND, NAND 등의 기본 게이트를 이해하고 진리표를 작성하고 회로도를 그린 후 HDL이라는 언어로 코딩을 하여 칩을 구성하는 것이었다.

처음엔 별 생각이 없었다. “OR? 입력값 두개를 더하는거네”. “AND? 입력값 두개를 곱하는거네” 등등

이정도만 이해하고 HDL로 코딩해보니 그냥 바로바로 통과가 됐기 때문이다.

중요한건 진리표와 회로도를 직접 그려보고 이를 더욱 효율적으로 간소화하는 작업이었는데, 이걸 간과했던 것이다. (이때는 몰랐다 😎)

아무튼 그렇게 알맹이만 쏙 빼먹은 채로 진행을 하다보니 ALU를 구현하는데서 바로 벽을 만나버렸다.


캡처



이게무슨소리야


이 시점에 우리 둘은 뭔가 단단히 잘못됐다는 것을 깨닫게 됐다.

“아! 이렇게 공부해봐야 아무런 의미가 없다. 다시 처음으로 돌아가자. 하나를 하더라도 제대로 해보자.”


관련 자료들을 최대한 찾아보니 우리가 이번 스터디의 1장을 어떻게 진행해야 하는지를 알 수 있었고, 새로운 방식을 적용해 이 책의 1장부터 다시 시작했다.

방법은 이랬다.


  1. 논리 게이트의 진리표를 먼저 그린다.
  2. 진리표를 토대로 논리식을 도출한다.
  3. 논리식을 더욱 간소화할 수 있는지 분석한다.
  4. 더 이상 간소화할 수 없다고 생각되면 회로도를 그려본다.
  5. 이를 HDL로 옮긴다.
  6. 테스트 코드를 실행하고 통과되는지 확인한다.


입력이 2개인 XOR 게이트를 기준으로 설명하자면 이렇게 진행이 됐다.

배타적 논리합(XOR)은 두 입력이 다를 경우에만 참을 반환한다. 이것을 이해하고 진리표를 작성한다.


image


이제 작성한 진리표에서 논리식을 도출해낸다.

이 때 결과가 1이 나오는 경우만 따져서 식을 도출해야 간소화가 조금 더 편해진다.


image


위의 수식은 내 생각에 더 이상 간소화할 수 없으므로 회로도로 그린다.

(첨언하자면, 이 수식을 진리표로 그려보면 XOR의 진리표와 같음을 알 수 있다.)


image


이것을 HDL로 옮긴다.


CHIP Xor {
    IN a, b;
    OUT out;

    PARTS:
    Not(in=a, out=nota);
	Not(in=b, out=notb);
	And(a=a, b=notb, out=o1);
	And(a=nota, b=b, out=o2);
	Or(a=o1, b=o2, out=out);
}


그리고 위에 작성한 코드를 밑바닥부터 만드는 컴퓨팅 시스템에서 제공해주는 시뮬레이터에 세팅하고, 같이 제공 된 테스트 코드도 함께 세팅한 다음 실행해본다.


image

…성공! 이때의 희열은 이루 말로 할 수 없었다!


(대략 무지막지한 노가다와 삽질의 흔적들… 수십장 되는 듯 하다… 😱)

KakaoTalk_20211026_221829552_05


KakaoTalk_20211026_221829552


KakaoTalk_20211026_221829552_01


KakaoTalk_20211026_221829552_02


KakaoTalk_20211026_221829552_03


KakaoTalk_20211026_221829552_04


“이걸 배워서 어디다 써먹어야 될까?” 같은 생각을 떠나서 그냥 참 재미있다고 느꼈다.

CS를 공부하면서 이런 감정을 느껴본건 처음이었던 것 같다. 왜일까?

이것을 해내기 위해 수시간 동안 디지털논리회로 과목의 기초를 보며 명제, 카르노맵, 분배 법칙, 결합 법칙, 흡수 법칙, 교환 법칙, 드모르간의 법칙 등과, 컴퓨터가 사칙연산을 어떻게 덧셈으로만 다 해낼 수 있는지.

반가산기, 전가산기 등이 왜 중요한지 등등 정말 많은 개념을 학습해야만 했다.

아무튼 이런 개념들을 학습하고 즉시 코드에 적용 해 결과물을 받아볼 수 있다는 것은, 내가 현재 학습하고 있는 지식들이 죽은 지식이 아닌 살아있는 지식임을 느끼게 해주었고, 이것이 내게 흥미와 재미로 다가왔던 것 같다.


한참동안 2진수에 대해 학습하다 문득 승재님과 재미있는 것을 하나 했다.

우리가 학습한 내용을 응용해보면 홀수 짝수 연산을 조금 더 효율적으로 할 수 있지 않을까 였다.


역시 많은 삽질이 있었지만, 우리가 깨달은 내용은 이랬다.

2진수의 각 자릿수는 2의 거듭제곱으로 표현되는데, 여기서 가장 오른쪽 자리수만이 유일하게 2의 거듭제곱이면서도 홀수인 1을 나타낸다. (2^0 == 1)

즉, 2의 거듭제곱을 쭉 나열해보면 1,2,4,8,16,32,64 … 여기서 1을 제외한 모든 수가 짝수이다.

나중에 안 사실인데 이를 최하위비트(Least Significant Bit, LSB)라고 부른다고 하더라.

아무튼 예를 들면 다음과 같다.


짝수인 8을 2진수로 변환하면 1000 이다.

홀수인 9를 2진수로 변환하면 1001 이다.

짝수인 10을 2진수로 변환하면 1010 이다.

홀수인 11을 2진수로 변환하면 1011 이다.


여기서 어떤 법칙을 발견했다.

최하위비트가 1이면 홀수, 0이면 짝수인 것.

이 발상에 착안 해 인수가 짝수라면 true를 반환하고 홀수라면 false를 반환하는 함수를 자바로 구현하니 다음과 같은 코드가 나왔다.


return (number & 1) == 0;


어떻게 위와 같은 코드가 나오게 되는 것일까?


number는 자바의 비트연산자로 인해 2진수로 형변환이 되며, 1 역시 같은 비트를 갖는 2진수로 형변환이 된다.

즉, number=8이라면 이는 1000으로 형변환이 되며, 1은 0001로 형변환이 되게 되며, 이를 논리곱연산(&) 하면 각 자릿수끼리 곱셈하게 된다.


image


number=9인 경우를 보자.

9는 1001로 변환되고, 1 역시 같은 비트수를 갖는 0001로 변환된다.


image


즉, 두 2진수의 논리곱 결과가 10진수 0이라면 짝수, 1이라면 홀수라는 결론이 나온다.

그리고 이 코드의 성능이 궁금해져 JMH로 성능 측정을 해봤다.

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class EvenNumberChecker {

    @Benchmark
    public void isEvenNumber(Blackhole bh) {
        for (int i = 0; i < 100_000_000; i++) {
            bh.consume(isEvenNumber(i));
        }
    }

    public static boolean isEvenNumber(final int number) {
        return number % 2 == 0;
    }

    @Benchmark
    public void isEvenNumberWithBitwiseOperation(Blackhole bh) {
        for (int i = 0; i < 100_000_000; i++) {
            bh.consume(isEvenNumberWithBitwiseOperation(i));
        }
    }

    public static boolean isEvenNumberWithBitwiseOperation(final int number) {
        return (number & 1) == 0;
    }

}


결과는 아래와 같았다.


Result "io.shirohoo.benchmarks.EvenNumberChecker.isEvenNumber":
  4.961 ±(99.9%) 1.602 ops/s [Average]
  (min, avg, max) = (4.872, 4.961, 5.048), stdev = 0.088
  CI (99.9%): [3.359, 6.563] (assumes normal distribution)
  
Result "io.shirohoo.benchmarks.EvenNumberChecker.isEvenNumberWithBitwiseOperation":
  4.986 ±(99.9%) 1.287 ops/s [Average]
  (min, avg, max) = (4.920, 4.986, 5.061), stdev = 0.071
  CI (99.9%): [3.699, 6.273] (assumes normal distribution)


일단 유의미한 성능차이는 없는 것 같긴 한데, 비트연산으로 작성한 코드가 평균적으로 더 좋은 성능을 낸다고 봐도 될 것 같다고 생각했다.


이 스터디를 진행하며 느낀 것은 현재 학습하는 CS 지식들이 개발에 즉각적인 도움을 주지는 않는다는 것이다.

다만, 이러한 것들을 위해 계속 해야만 하는 고강도의 논리적인 사고가 내 사고를 점점 효율적으로 만들어준다는 것은 느낄 수 있었다.

이런 학습을 장기간 지속할 수 있다면 내 개발 스타일이 점점 더 세밀하게 좋아질 것 같다는 생각이 들었다.



© 2022. All rights reserved.