0. 들어가기에 앞서...


솔직히 너무 아이큐 테스트 같았던 실습이었다. 막 몇 시간씩 한 function에 매달리고 있는 학생들도 있었는데, 내가 느끼기엔 한 1시간 정도 고민하다가 도저히 떠오르지 않으면 구글링 하는게 더 나을 듯 하다.



1. bitAnd


/*
* bitAnd - x&y using only ~ and |
* Example: bitAnd(6, 5) = 4
* Legal ops: ~ |
* Max ops: 8
* Rating: 1
*/
int bitAnd(int x, int y) {
    return ~(~x | ~y); //드모르간의 법칙 이용
}


(...)



2. getByte


/*
* getByte - Extract byte n from word x
* Bytes numbered from 0 (LSB) to 3 (MSB)
* Examples: getByte(0x12345678,1) = 0x56
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 6
* Rating: 2
*/
int getByte(int x, int n) { //추출하고자 하는 byte에 해당하는 2자리의 16진수가
least significant bit 부터 위치하도록 해주는 function.
    int shift = n << 3; //n이 0~3이면 0, 8, 16, 24만큼 right shift 해주기 위함.
    x = x >> shift;
    return x & 0xFF; //least significant 2자리 16진수를 추출
}


0x11223344라는 32비트 수가 있다고 하자.

그러면 n이 0~3일 때 각각 0x44, 0x33, 0x22, 0x11이 값이 된다.


따라서 n에 8을 곱한 만큼 right shift 한 뒤 0xFF의 mask를 씌워 추출한다고 생각하면 된다.



3. logicalShift


/*
* logicalShift - shift x to the right by n, using a logical shift
* Can assume that 0 <= n <= 31
* Examples: logicalShift(0x87654321,4) = 0x08765432
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 20
* Rating: 3
*/
int logicalShift(int x, int n) { //새로 생성된 부분에 해당하는 bit만 0으로 모두 바꾸어줌
    int mask = ((1 << 31) >> n) << 1; //1111000........과 같은 형태의 mask 생성
    x = x >> n; //Arithmetic shift 실행
    return ~mask & x; //~overlap과 x의 bitAnd 연산 실행
}


기본적으로 arithmetic shift가 적용되므로 sign bit가 1일 경우 right shift를 하면,

shifting된 bit들이 모두 1이 된다. 그 부분을 모두 0으로 바꾸어 주면 된다.



4. bitCount


/*
* bitCount - returns count of number of 1's in word
* Examples: bitCount(5) = 2, bitCount(7) = 3
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 40
* Rating: 4
*/
int bitCount(int x) { //16진수 한개 단위로 1인 bit의 개수를 세고, 이를 더해주는 방식.
    int count = 0x11 | (0x11 << 8); //0x00001111 생성.
    int sum = 0;
    int mask = 0xF | (0xF << 8); //0x00000F0F 생성.
    count = count | (count << 16); //0x11111111 생성.
    sum = x & count; //각 16진수 1개의 첫번째 bit가 1이면 1을 더해주고, 아니면 그대로 유지.
    sum = sum + ((x >> 1)&count); //각 16진수 1개의 두번째 bit가 1이면 1을 더해주고,
아니면 그대로 유지.
    sum = sum + ((x >> 2)&count); //위와 동일
    sum = sum + ((x >> 3)&count); //위와 동일
    sum = sum + (sum >> 16); //0~15번째 bit에서 1인 bit의 수와, 16~31번째 bit에서 1인
bit의 수의 합이 0~3번째 16진수 숫자에 저장되도록 함.
    sum = (sum&mask) + ((sum >> 4)&mask);
    return (sum & 0xFF) + (sum >> 8);
}


제일 쉬운 방식은 31번 shift를 해주면서 least significant bit가 1인지 검증하고, 그 횟수를 세면 된다.

근데 이런 방식으로 하면 Max ops를 한참 넘게 된다..


1비트를 한 단위로 보지 않고, 4비트(16진수 한 개)를 한 단위로 보면 코드를 짧게 줄일 수 있다.


5. bang


/*
* bang - Compute !x without using !
* Examples: bang(3) = 0, bang(0) = 1
* Legal ops: ~ & ^ | + << >>
* Max ops: 12
* Rating: 4
*/
int bang(int x) {
    int notzero = x | (~x + 1); //0이 아니면 자신과, 자신의 음수와의 bitOr 연산을 했을 때
sign bit 가 1이 됨. (0x80000000의 경우에도 해당함.)
    return (notzero >> 31) + 1; //notzero >> 31을 한 결과는 0이 아닌 경우 0xFFFFFFFF,
0인 경우 0이 되므로 여기에 1을 더한 결과를 출력하면 됨.
}


주석의 내용이 전부이다.



6. tmin


/*
* tmin - return minimum two's complement integer
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 4
* Rating: 1
*/
int tmin(void) {
    return 1 << 31; //가장 작은 수는 0x80000000.
}


쉬어가는 코너.



7. fitsBits

/*
* fitsBits - return 1 if x can be represented as an
* n-bit, two's complement integer.
* 1 <= n <= 32
* Examples: fitsBits(5,3) = 0, fitsBits(-4,3) = 1
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 2
*/
int fitsBits(int x, int n) { //left shift를 실행한 뒤 다시 right shift를 실행해서
원래 수와 같아지는 지를 비교해보는 function.
    int shift = 33+ (~n); //left shift를 32-n번 시키기 위함.
    int value = (x <<shift) >> shift; //left shift 실행한 뒤, 다시 right shift로 복원.
    return !(value ^ x); //복원된 숫자와 x가 일치하면 1, 아니면 0을 출력.
}


left shift 했다가 다시 right shift로 비트들을 원상복귀 시켰을 때, 원래의 수와 일치하는 지를 보면 된다.


방법은 간단한데 생각하는 게 어려웠던 문제.



8. bitAnd


/*
* divpwr2 - Compute x/(2^n), for 0 <= n <= 30
* Round toward zero
* Examples: divpwr2(15,1) = 7, divpwr2(-33,4) = -2
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 15
* Rating: 2
*/
int divpwr2(int x, int n) {
    int negative = x >> 31; //음수일 경우 0xFFFFFFFF가, 아닐경우 0이 저장됨.
    int value = x + (negative & ((1 << n) + negative)); //bias를 원래 x에 더해준 값을
value에 저장.
    return value >> n;
}


관건은 x가 음수일 경우이다. round toward zero이므로, 그냥 x>>n을 결과로 낼 경우

x가 음수일 때는 정답과 함수의 결과값이 차이가 나게 된다.


따라서 rounding을 고려하여 x에 bias를 더해주는 작업이 필요하다.



9. negate


/*
* negate - return -x
* Example: negate(1) = -1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 5
* Rating: 2
*/
int negate(int x) {
    return ~x + 1; //2's complement에서 x의 음수는 ~x + 1임.
}


tmin과 마찬가지로 쉬어가는 코너.



10. isPositive


/*
* isPositive - return 1 if x > 0, return 0 otherwise
* Example: isPositive(-1) = 0.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 8
* Rating: 3
*/
int isPositive(int x) {
    int signconverse = !(x >> 31); //음수일 경우 0, 아닐 경우 1이 저장됨.
    return !x^signconverse; //양수일 경우 (0^1)=1이, 0이나 음수일 경우
(1^1)=0, (0^0)=0이 출력됨.
}


0 이상인지를 알려면 그냥 sign bit로 판별하면 되는데, 양수만을 판별해야 되서 살짝 까다로운 문제이다.



11. isLessOrEqual


/*
* isLessOrEqual - if x <= y then return 1, else return 0
* Example: isLessOrEqual(4,5) = 1.
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 24
* Rating: 3
*/
int isLessOrEqual(int x, int y) {
    int xsign = (x >> 31) & 1;
    int ysign = (y >> 31) & 1;
    int dif = xsign^ysign;
    int valuesign = ((~x + 1) + y) >> 31;
    return (dif & xsign) | (!(dif | valuesign));
    // 첫번째 경우는 y는 양수이고, x는 음수인 경우에 대해 overflow를 방지하기 위함.
    // 두번째 경우는 x와 y가 같은 sign bit를 가졌을 때
y-x의 sign bit를 따져서 양수인지 판별.
}


그냥 y에서 x를 뺀 다음에 sign bit로 판별하면 되는 거 아닌가~? 라고 생각했다면 오산이다.


만약 y가 양수이고, x가 음수라면? 당연히 정답은 1이 나와야 하지만, overflow에 의해 0이 나올 수 있다.

이 문제의 관건은 x와 y의 sign bit가 다를 때 overflow를 고려해 주는 것이다.



12. ilog2


/*
* ilog2 - return floor(log base 2 of x), where x > 0
* Example: ilog2(16) = 4
* Legal ops: ! ~ & ^ | + << >>
* Max ops: 90
* Rating: 4
*/
int ilog2(int x) { //leftmost 1의 위치를 찾는 function.
    int count = 0x11 | (0x11 << 8);
    int sum = 0;
    int mask = 0xF | (0xF << 8);

    //1st section. leftmost 1부터 least significant bit까지 전체를 1로 채움.
    x = x | (x >> 1);
    x = x | (x >> 2);
    x = x | (x >> 4);
    x = x | (x >> 8);
    x = x | (x >> 16);

    //2nd section. 1의 개수를 bitcount한 뒤 1을 빼준 값을 출력.
    count = count | (count << 16);
    sum = sum + (x&count);
    sum = sum + ((x >> 1)&count);
    sum = sum + ((x >> 2)&count);
    sum = sum + ((x >> 3)&count);
    sum = sum + (sum >> 16);
    sum = (sum&mask) + ((sum >> 4)&mask);
    return ((sum & 0xFF) + (sum >> 8)) + (~0);
}


bitCount와 동일한 문제로 볼 수 있다.


leftmost 1의 위치에서 1을 뺀 것이 답이라는 건 알지만, 이를 어떻게 구현하는 지가 고민이다.

정답은 leftmost 1부터 least significant bit까지를 다 1로 채운 뒤, bitCount를 시행하면 위치를 알 수 있다.(!)


코드를 보면 2 section으로 나뉘어 있는데, 그에 대한 설명은 주석과 같다.



13. float_neg


/*
* float_neg - Return bit-level equivalent of expression -f for
* floating point argument f.
* Both the argument and result are passed as unsigned int's, but
* they are to be interpreted as the bit-level representations of
* single-precision floating point values.
* When argument is NaN, return argument.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 10
* Rating: 2
*/
unsigned float_neg(unsigned uf) {
    int result = 0x80000000 ^ uf; //sign bit만 바꿔줌.
    int exp = 0x7F800000 & uf;
    int frac = 0x007FFFFF & uf;
    if((exp == 0x7F800000) && frac) //exp가 11111111이고, frac이 0이 아닌 경우 NaN
        return uf;
    return result; //NaN이 아닌 경우에는 result를 출력.
}


unsigned uf 자체를 floating point expression으로 생각한 뒤 sign bit만 바꿔주는 함수이다.


NaN일 경우만 고려해주면 되는데, NaN은 exp bit가 모두 1이고, frac bit가 0이 아닌 경우 이므로, if문으로 쉽게 예외 처리해주면 된다.



14. float_i2f


/*
* float_i2f - Return bit-level equivalent of expression (float) x
* Result is returned as unsigned int, but
* it is to be interpreted as the bit-level representation of a
* single-precision floating point values.
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
unsigned float_i2f(int x) {
    int sign, bit, exp, frac, fracmask, round;
    sign = x & 0x80000000;
    fracmask = 0x7FFFFF;
    if(x == 0)
        return 0;
    else if(x == 0x80000000){
        exp = 158;
        return 0xCF000000;
    }
    
    else{
        if(sign) //x가 음수라면 먼저 양수로 바꿔줌.
            x = -x;
        bit = 1;
        while((x>>bit) != 0) //x의 leftmost bit를 찾기 위해 while문 삽입.
            bit++;
        bit--;
        exp = bit + 127;

        x = x << (31 - bit);
        frac = fracmask & (x >> 8);

        if(bit > 23){ //부동소수점에서 rounding 계산.
            round = x & 0xFF;
            if((round > 128) || ((round == 128) && (frac & 1))){
                frac++;
                if(frac >> 23){
                    exp++;
                    frac = 0;
                }
            }
        }
    }
    return sign | (exp << 23) | frac;
}


(...)



15. float_twice


/*
* float_twice - Return bit-level equivalent of expression 2*f for
* floating point argument f.
* Both the argument and result are passed as unsigned int's, but
* they are to be interpreted as the bit-level representation of
* single-precision floating point values.
* When argument is NaN, return argument
* Legal ops: Any integer/unsigned operations incl. ||, &&. also if, while
* Max ops: 30
* Rating: 4
*/
unsigned float_twice(unsigned uf) {
    int exp = 0x7F800000 & uf;
    int sign = 0x80000000 & uf;
    if(exp == 0x7F800000)
        return uf;
    else if(exp == 0) //Denormalized를 2배할 때는 sign bit를 제외한 나머지 bit들을
1칸씩 left shift하면 됨.
        return sign | (uf << 1);
    else //Normalized를 2배할 때는 exp에 1을 더해주면 됨.
        return uf + 0x00800000;
}


floating point를 공부했다면 denormalized와 normalized의 차이에 대해 알 것이다.


uf는 denormalized, normalized, NaN의 3가지 값을 가질 수 있다.

이 때 uf가 denormalized라면 sign bit를 제외한 나머지 bit을 1칸 left shift 하는 것이 2배하는 효과를 낸다.


uf가 normalized라면 그냥 exp에 1을 더해주는 것이 2배하는 효과를 낸다.