Post List

2018년 5월 27일 일요일

Dead Lock, Virtual Memory, Round Robin Scheduling

[OS] 복습

내가 OS에서 상당히 좋은 성적을 받았고 나름 이해도가 높다고 생각햇는데
남에게 어떠한 용어에 대해 명확하게 설명하는 것은 좀더 구체적이고 디테일하게 개념을
이해해야 한다는 것을 뼈저리게 느꼈다.


Dead Lock이란..


내가 주로 강의자료를 보면서 공부하기 때문에 강의자료를 펴보았는데 설명이 이렇게 되어있다.

두개 이상의 process/thread 가 서로 어떠한 이벤트를 해주기를 기다리는 것이다
예를들어

이런 상황에서 S, Q 모두 1로 세팅되어 있다고 생각하자. P_0가 전부 수행되고 P_1이 수행되고를 반복하는 것은 아무 문제가 없다. 하지만 multi process 환경에선 이러한 순서를 보장해 줄 수 없다. P_0, P_1 이 거의 동시에 들어가서 S, Q의 값이 0이 된다면 서로 semaphore의 값을 올려주길 기다린다. 따라서 진행이 되지 않는데 이런 상태를 Deadlock(교착상태) 라고 한다. 


Virtual Memory 란 실제 물리적으로 존재하는 메모리가 아닌 논리적으로 존재하는 메모리 주소공간을 뜻하는데 어렴풋이 들으면 무슨 뜬구름 잡는 소린가 싶을 수 있다. 

우리가 프로그래밍을 할때 변수에 접근하거나 특정 메모리 공간에 접근하는데 이는 실제 물리 메모리 공간이 아니다. 

역시 예를 들어 설명하는 것이 좋을 것 같다.

우리의 메모리가 고작 8MB 뿐이라고 생각해보자. 여러 프로그램을 돌리다가 메모리가 꽉차면 더 이상 메모리에 적재가 되지 않을까? 그렇지 않다. 메모리에 적재된 내용중에 일부가 디스크로 옮겨지고 해당 내용이 빠진 메모리 공간를 이용할 수 있다. 나중에 메모리 공간에 여유가 생기거나 접근하게 되면 디스크로 옮겨둔 내용을 다시 메모리에 적재해 수행시킬 수 있다. 이러한 과정을 swap out / swap in 이라 하고, 이 방법으로 실 메모리보다 더 큰 메모리를 사용할 수 있는 것이다.


마지막으로 Round Robin Scheduling...

위키피디아에서 복사해오자.
라운드 로빈 스케줄링(Round Robin Scheduling, RR)은 시분할 시스템을 위해 설계된 선점형 스케줄링의 하나로서, 프로세스들 사이에 우선순위를 두지 않고, 순서대로 시간단위(Time Quantum)로 CPU를 할당하는 방식의 CPU 스케줄링 알고리즘이다.

즉 프로세스들에게 일정 time quantum을 주어 그 시간이 지나면 다른 프로세스에게 CPU를 할당하는 것인데 이럴 경우 각 프로세스마다 응답시간이 줄어드는 장점이 있다. 또 priority에 구애받지 않고 골고루 프로세스를 수행하리 때문에 starvation도 일어나지 않는다. 하지만 계속해서 context switching이 일어나기 때문에 오버헤드가 크다.


2018년 5월 24일 목요일

샤로수길 새우당

여자친구의 노트북이 고장나서 봉천동 삼성 서비스 센터 갔다 샤로수 길 새우당이라는 곳을 가봄.

사람이 좀 있어서 한 30분 정도 기다렸었음.

가게는 요로코롬 생겼고
내가 먹은건 요 불새우
여자친구가 먹은건 간장새우
가격은 대충 만원정도 하는데 적정가인듯 하다.
불새우는 꽤 매우니까 매운거 좋아하는 사람들만 먹자.


2018년 5월 22일 화요일

BST (Binary Search Tree)

[자료구조 복습]

자료구조를 복습하면서 트리 위주로 보고 있는데 그 이유는 정말 많은 트리가 있고 실제로 범용성이 크기 때문이다.

BST 트리를 다시 볼때 개념도 어렵지 않고 이름부터 쉬워보여서 사실 쉽게 넘어갈 줄 알았다. 직접 구현을 해보려니 미천한 나의 실력으로 정말 오래 걸렸다. 재귀함수를 기피하다 보니까 코드도 지저분해지고...

Binary Search Tree란 다음의 성질을 만족하는 이진트리다.

  • Each node has exactly one key and the keys in the tree are distinct
  • The keys (if any)  in the left subtree are smaller than the key in the root
  • The keys (if any)  in the right subtree are smaller than the key in the root
  • subtrees are also BST

즉 leftchild < parent < rightchild 형태를 띄는 이진트리이다.

BST operations
  • search
  • insert
  • delete

역시 코드를 보자.



#define ITER 0
#define RECUR 1
#define SEARCH ITER

typedef struct _NODE {
    int key;
    struct _NODE* leftchild;
    struct _NODE* rightchild;
    struct _NODE* parent;
}BST;

BST* bst_root;

BST* r_search(BST* subtree,int key) {
    if (!subtree) return NULL;
    if (subtree->key == key)
        return subtree;
    if (key < subtree->key)
        return r_search(subtree->leftchild, key);
    return r_search(subtree->rightchild, key);
}

BST* i_search(BST* subtree, int key) {
    while (subtree) {
        if (key == subtree->key)
           return subtree;
        if (key < subtree->key)
            subtree = subtree->leftchild;
        else
            subtree = subtree->rightchild;
    }
    return NULL;
}

BST* m_search(BST* subtree, int key) {
    while (subtree) {
        if (key == subtree->key)
            return NULL;
        if (key < subtree->key) {
            if (subtree->leftchild == NULL)
                return subtree;
            subtree = subtree->leftchild;
        }
        else {
            if (subtree->rightchild == NULL)
                return subtree;
            subtree = subtree->rightchild;
        }
    }
    return NULL;
}

search 의 경우 정말 간단하다.

왼쪽 오른쪽을 비교해가며 내려가면서 key 값을 비교하기만 하면된다.
BST가 균형이 잘 맞춰져 있다면 시간복잡도는 O(log n) 에 가깝고 한쪽으로 치우친 리스트 형태를 띄면 최악의 경우 O(n) 이다. 즉 트리의 깊이를 d라 할때 O(d)라 볼 수 있다.

search가 3개나 있는 이유는 recursive, iterative, 그리고 insert에 사용할 modified search가 필요하기 때문이다.


void insert(BST** root, int key) {
    BST* temp=m_search(*root,key);
    if (temp || !(*root)) {
        BST* new_node = (BST*)malloc(sizeof(BST));
        new_node->key = key;
        new_node->leftchild = NULL;
        new_node->rightchild = NULL;
        new_node->parent = NULL;
        if (temp) {
            if (key < temp->key)
                temp->leftchild = new_node;
            else
                temp->rightchild = new_node;
            new_node->parent = temp;
        }
        else *root = new_node;
    }
}

insert의 경우 일단 BST에 key값이 존재하는지 판별한다. 이미 존재 한다면 insert 할 필요가 없고 존재하지 않는다면 m_search로 부터 반환받은 위치의 leftchild 혹은 rightchild 에 insert한다.

위치를 반환한 후 나머지 작업은 O(c) 이므로 총 시간 복잡도는 O(h)이다.

void bst_delete(int key) {
    BST* del_node=i_search(bst_root,key);
    BST* parent = del_node->parent;
    BST *temp_root;

    if (!del_node) {
        fprintf(stdout, "not in the tree\n");
        return;
    }
 
    /* DEL_NODE HAS 0 OR 1 CHILD */
    if (del_node->leftchild == NULL) {
        BST *temp = del_node->rightchild;
        free(del_node);
        if (!parent) {
            bst_root = temp;
            if(temp)
                temp->parent = NULL;
            return;
        }
        if (parent->leftchild->key == del_node->key)
            parent->leftchild = temp;
        else
            parent->rightchild = temp;
        return;
    }
    else if (del_node->rightchild == NULL) {
        BST *temp = del_node->leftchild;
        free(del_node);
        if (!parent) {
            bst_root = temp;
            if(temp)
                temp->parent = NULL;
            return;
        }
        if (parent->leftchild->key == del_node->key)
            parent->leftchild = temp;
        else
            parent->rightchild = temp;
        return;
    }
    /* DEL_NODE HAS 2 CHILD */
    else if (del_node->leftchild && del_node->rightchild) {
        BST* curr=del_node->leftchild;
        while (curr->rightchild) {
            curr = curr->rightchild;
        }
        del_node->key = curr->key;
        curr->parent->rightchild = curr->leftchild;
        if(curr->leftchild)
            curr->leftchild->parent = curr->parent;
        free(curr);
    }
}

이 빌어먹을 deletion을 짜느라 고생좀 했다(사실 오류가 있을지도 모른다)

insertion 과 다르게 살짝 복잡한데 delete 할 node의 자식 수에 따라 다르게 행동하기 때문이다.

  1. delete 할 node에 child node가 없다면 그냥 free해버린다.
  2. delete 할 node에 child node가 하나 있다면 delete 할 node 의 위치에 child를 둔 후 free한다
  3. delete 할 node에 child node가 두개 있다면 왼쪽 subtree에서 가장 큰 node나 오른쪽 subtree에서 가장 작은 node (A 라 하자)를 delete할 node의 key를 swap한 후 A의 위치에는 A의 left child를 둔다
사실 재귀함수로 짜인 여러 예쁜 코드들을 봤지만 interative로 작성해보고 싶어 시도했다가 정말 시간을 많이 버렸다.

2018년 5월 21일 월요일

Heap

[자료구조 복습]
Heap이란 다음의 성질을 만족하는 이진트리다.

Heap : Heap is a complete binary tree in which the key value in each node is no smaller(larger) than the key values in its children.
즉 모든 노드에 대해 부모는 자식보다 항상 크거나, 항상 작다면 max heap 또는 min heap인 것이다.
Heap은 priority queue 구현에 자주 이용된다고 한다(난 잘 모름).
일단 Heap은 complete binary tree이기 때문에 배열로 구현해도 memory 낭비가 크지 않다. 이 경우에 search는 O(n)에 가능하다.
Heap operations
  • push
  • pop
Heap 연산은 기본적으로 두 가지 있는데 push와 pop이다. 코드가 어렵지 않으니 코드부터 보자.

#define HEAP_MAX_SIZE 100
int heap[HEAP_MAX_SIZE];
int heap_size = 0;
전역변수 선언

void heap_insert(int value) {
    int i;
    if (heap_size== HEAP_MAX_SIZE - 1) {
        fprintf(stdout, "Heap is full\n");
        return;
    }
    heap_size++;
    i = heap_size;
    while ((i != 1) && value > heap[i>>1]) {
        heap[i] = heap[i>>1];
        i >>= 1;
    }
    heap[i] = value;
}

insert가 동작하는 것을 보면 다음과 같다.

기존에 n까지 Heap에 저장되어 있다면 n+1 위치에 우선 삽입한다. 그 후 parent 와 비교하며 끌어 내리는 방식인데 이를 bubbling up이라고 한다.

이진트리이기 때문에 시간복잡도는 O(log n).


int heap_delete() {
    int parent=1, child=2;
    int res = heap[1];
    int temp;
    if (!heap_size) {
      fprintf(stdout,"Heap is empty\n");
      exit(EXIT_FAILURE);
    }
    temp = heap[heap_size--];
    while (child <= heap_size) {
      if (heap[child] < heap[child + 1] && child < heap_size)
          child++;
      if (temp >= heap[child]) break;
      heap[parent] = heap[child];
      parent = child;
      child <<= 1;
    }
    heap[parent] = temp;
    return res;
}

delete는 항상 root값을 pop하는 방식이다.

parent가 빌 경우 child 중에 한 노드를 parent로 올리는 것을 반복하며 채운다. 이때 맨 heap의 총 사이즈가 1 줄었기 때문에 마지막 노드가 이동해야 하는데 노드가 올라가면서 생기는 빈자리 중 알맞은 자리에 위치하게 된다.

delete 역시 시간복잡도는 O(log n).


void adjust(int root_idx, int size) {
    int child, root_value;
 
    root_value = heap[root_idx];
    child = root_idx<<1;
    while (child <= size) {
        if (child < size && (heap[child] < heap[child + 1]))
             child++;
        if (root_value > heap[child]) break;
        heap[child>>1] = heap[child];
        child <<= 1;
    }
    heap[child >> 1] = root_value;
}

void heap_sort() {
    for (int i = heap_size >> 1; i > 0; i--) {
        adjust(i, heap_size);
    }
    for (int i = heap_size - 1; i > 0; i--) {
        int temp=heap[1];
        heap[1] = heap[i + 1];
        heap[i + 1] = temp;
        adjust(1, i);
    }
}

만들어진 Heap을 정렬하고 싶을땐 어떻게 할까?

Heap sort의 첫번째 반복문이 하는 일은 난수가 저장된 배열을 heapify 하는 것이다. Subtree들을 아래서부터 위까지 Heap 형태로 가꾸는 방식이다.

이렇게 만들어진 Heap을 정렬하는데 Heap 형태로 저장된 값들을 배열형태로 정렬하는 것이다. 코드를 돌려보면 알겠지만 root값을 맨 뒤로 빼고 나머지 값들을 다시 힙 형태로 만든다. 이를 반복하면 전체 힙이 배열형태로 정렬된다.

Heap 정렬의 시간복잡도는 O(n) + O(nlog n) = O(nlog n).

2018년 5월 20일 일요일

Computer Organization and Design

#컴퓨터의 성능은 어떻게 정의될까

용어 정리부터 해보자

response time(execution time) : the total time required for the computer to complete a task

throughput(bandwidth) : measure of performance, the number of tasks completed per unit time

당연하게도 execution time 이 짧을수록 성능이 좋고 성능 performance를 다음과 같이 정의할 수 있다.

image

하지만 processor 은 하나의 task만 수행하지 않고 여러 task를 나눠서 수행하기도 하고 execution time 자체가 I/O, OS overhead, memory access time등을 포함한다. 따라서 위와 같이 하나의 프로그램이 수행되기 까지의 elapsed time으로 성능측정을 하기에 문제가 있다.

CPU execution time(CPU time) : the actual time the CPU spends computing for a specific task

즉 I/O나 다른 프로그램을 돌리는데 걸리는 시간을 제외한 실제 소비한 시간만을 말한다.

그렇다면 CPU execution time은 어떻게 구할까

imageimage

영어로 되있어서 어려워 보일 수 있지만 사실 당연한 식이다.

프로그램 수행시간 동안 clock 이 뛴 횟수 : x (tick)

1 tick 당 걸리는 시간 : y (msec / tick) (단위는 주어지면 계산하면 된다)

위와 같은 상황이면 프로그램을 수행하는데 걸리는 시간은 x*y (msec) 가 된다.

clock rate은 clock cycle time 의 역수이니 아래의 식은 당연히 성립한다.

예제를 풀어보면 금방 감이 온다!


그렇다면 저 program은 무엇일까

program은 명령어들의 집합이다.

따라서 program을 수행하는데 걸리는 시간은 program을 이루는 Instructions 들을 수행하는데 걸리는 시간과 같고 다음과 같은 식을 생각할 수 있다. image

여기서 average clock cycle인 이유는 instruction마다 수행시간이 다르기 때문이다. 이 average clock cycle per instruction을 CPI 라고도 한다.

CPI(clock cycles per instruction) : average number of clock cycles per instruction

정리해보면 저번 포스팅의 CPU execution time은 CPI로 표현이 될 수 있다는 것이다. image

변형하고 응용해서 시험에 내기 딱 좋기 때문에 중간고사를 준비중인 학생은 여러 예제를 풀어보자.

Pattern matching, KMP algorithm

자료구조를 복습하느라 자료구조 책을 오랜만에 다시 펴봤다.

그러다 KMP algorithm이 눈에 들어왔다.

중간고사때 완벽히 풀어내지 못했던 기억이 새록새록 떠올랐다..

자료구조 수강 당시 완전 돌이었고(지금도 돌이지만) algorithm 이 많이 약하기 때문에 다시 봐야겠다고 생각했다.

우리가 string에서 어떤 pattern 이 있는지, 그 위치는 어딘지 구하려면 어떤 방식으로 구현할까

가장 쉬운방법은 brute force로 string[s_0] 부터 진행하면서 pattern과 일치하는지 비교하고 중간에 일치하지 않는다면 string[s_1] 부터 다시 비교를 시작하는 것이다.

이런식으로 구현을 한다면 시간 복잡도는 O(n*m) 이 된다는 것을 쉽게 생각할 수 있다.

이런 방식의 비효율은 어디서 발생할까.

string[i] 에서부터 시작해 string[i+n] 까지 이미 비교를 했음에도 불구하고 no match 가 발생하면 string[i+1] 로 돌아가 다시 비교를 한다는 점이다.

이를 개선한 방법이 KMP algorithm이다.

    int pmatch(char *string, char* pattern) {
        int i = 0, j = 0;
        int s_len = strlen(string);
        int p_len = strlen(pattern);

        while (i < s_len && j < p_len) {
            if (string[i] == pattern[j]) {
                i++; j++;
            }
            else if (j == 0) i++;
            else j = failure[j - 1] + 1;
        }
        return ((j == p_len) ? (i - p_len) : -1);
    }
   void fail(char *pattern) {
        int len = strlen(pattern);

        failure[0] = -1;
        for (int j = 1; j < len; j++) {
            int i = failure[j - 1];
            while ((pattern[j] != pattern[i + 1]) && (i >= 0))
                i = failure[i];
            if (pattern[j] == pattern[i + 1])
                failure[j] = i + 1;
            else
                failure[j] = -1;
        }
    }
   int main()
    {
        char string[MAX_STRING_LEN];
        char pattern[MAX_PATTERN_LEN];

        fscanf(stdin, "%s", string);
        fscanf(stdin, "%s", pattern);

        fail(pattern);
        pmatch(string,pattern);

        return 0;
    }
  • 중요한 것은 fail 함수이다. 

Kernel Memory Allocation

kernel 에서의 memory allocation 은 user space 에서와 무엇이 다를까?

기본적으로 api로 제공하는 함수는 kmalloc, vmalloc 이 있다.

kmalloc 의 경우 큰 특징이 있는데 바로 physical memory 에 바로 매핑이 된다는 것이다.

USER SPACE MEMORY ALLOCATION

흔히 사용자 혹은 프로그래머가 user space에서 malloc과 같은 함수를 호출하게 되면 process 마다 갖고있는 process address space에서 메모리를 할당받는다. 이는 실제 physical memory에 할당 받은 것이 아닌 가상주소공간을 할당받은 것이기 때문에 첫 참조시 page table을 참조해 memory에 없다는 것을 확인 후 page fault가 일어나고 그 후 매핑이 된다. 이때 physical memory에서의 연속성은 보장할 수 없다.

KERNEL SPACE MEMORY ALLOCATION

kmalloc의 경우 바로 physical memory를 할당받는 것이기 때문에 물리적 주소공간을 초과할 수 없다는 특징이 있다. 장점으로는 실제 주소 공간이 매핑되기 때문에 연속한 주소공간을 할당 받았다면, 물리적으로 연속적이다. DMA를 수행할 경우 device는 특정 영역내의 연속한 주소공간을 사용하기 때문에 kmalloc을 사용한다.

kmalloc의 다른 특징은 flag 설정이 가능하단 것인데 간단하게 3개만 보고 가자.(아는게 3개 뿐이다)

flag는 kmalloc(size_t size, int flag)의 두번째 parameter로 GFP_KERNEL의 경우 memory allocation 이 가능하지 않을 경우 해당 request를 보낸 process를 wait시킨 후 가용할 시 memory allocation을 해준 후 process wakeup을 시킨다. 이 flag를 사용할 때 kernel 내부의 작업이 process context가 아니라면 커널이 죽어버릴 수도 있다는 점을 주의하자

GFP_ATOMIC의 경우 memory allocation이 불가능 할 경우 NULL을 return 해준다.

vmalloc 의 경우엔 user space 에서의 memory allocation 과 유사하다. kernel virtual address space 에 연속적인 공간을 할당한다. 따라서 실제 가용 memory 보다 더 많은 영역을 할당 받을 수 있지만 DMA 와 같은 용도로는 부적합하다.

openCL & GPU programming

openCL을 수치컴퓨팅을 수강하면서 접하게 되었다.

1. 왜 GPU를 사용할까?

GPU의 아키텍처를 생각해보자.

GPU는 상당히 많은 processor 들과 메모리로 이루어져 있다.

사진은 radeon 계열의 GPU 내부구조이다. (내부 구조는 제품, 아키텍처 별로 다를 수 있다)

general purpose processing 에 초첨을 맞춰 CU를 들여다 보면

  • SIMD unit
  • Register
  • Local Data share
  • blog

로 이루어져 있다.

SIMD unit

SIMD unit은 16개의 lane으로 이루어져 있고 한개의 SIMD unit가 wavefront 혹은 warp를 담당한다. 즉 CU에 하나의 work group(thread block) 이 할당되면 work group내의 wavefront들이 4개의 SIMD unit에 4 cycle에 걸쳐 연산되는 것이다.

Register

Register는 각 SIMD unit마다 64KB이 있고 따라서 CU안에 64K의 32bit register가 있다.

Local Data Share

Local Data Share은 work group 내에서 데이터를 서로 공유할 수 있는 영역으로 L1 cache 급의 성능을 보인다. Global memory를 접근하는 것은 비용이 크므로 프로그래밍시 local memory를 적절히 쓰는 것이 중요하다.

특정 상황에서 CPU를 쓰는 것보다 GPU를 이용해 가속을 하면 얼마나 성능향상이 되는지 감이 잘 오지 않을 수 있다. 그래서 학교 과제로 나온 cloth simulation을 GPU, CPU로 각각 수행시켜 보았다.

GPU accelerated cloth simulation

CPU cloth simulation

내가 짠 코드는 분명 구데기 같은 코드지만 차이를 확연하게 느낄 수 있다

Resource Allocation in GPU

performance estimation : occupancy

GPU를 이용해 가속을 할 때 생각보다 성능이 많이 개선되지 않았다고 느낄 때가 많다. 또 성능 평가를 어떻게 해야하는지도 모르고 막연히 빨라지겠지 기대할 수 도 있다.

성능에 영향을 주는 중요한 요인 중 하나가 occupancy다. 말 그대로 점유율을 뜻하는데 이전 포스팅에서 적었듯이 GPU에서 global memory를 접근하는 것은 매우 큰 비용이 발생한다. 따라서 context switching이 원활하게 일어 날 수 있도록 CU에 Work Group(WG)이 많이 올라와 있는것이 유리하다. 결국 WG를 많이 올리는 동시에 kernel program이 빠른 속도로 수행되는 적절한 지점을 찾는 것이 중요하다.

WG를 무작정 많이 올리는 것이 가능할까?

당연히 resource는 유한하고 제약이 있다. Radeon R9 290X를 두고 예를 들어보자.

CU안의 resource를 보면 다음과 같다.

vector unit

vGPR(vector General Purpose Register)

sGPR (scalar General Purpose Register)

LDS (Local Data Share)

  • vector unit 당 최대로 올릴 수 있는 WF(WaveFront) 가 10개로 제한되어 있다. 따라서 CU당 총 40개의 WF가 올라 갈 수 있는데 WG가 4WF로 이루어져 있다면 총 10개의 WG이 CU에 올라갈 수 있는 것이다.


256kB vGPR : 4 * 64 * 4 * 256

순서대로 4 vector unit , 64 lane, 4byte, 256 vGPR를 의미한다.

  • 정리해보면 64K vGPR이 존재한다. 즉 최대로 사용할 수 있는 vGPR이 64K 개라는 것인데 만약 kernel에서 하나의 WI(Work Item)가 42개의 vGPR를 사용하고, 하나의 WG에 4WF로 이루어져 있다면 하나의 WG가 42_256 vGPR을 필요로 한다. 따라서 64K / 42_256 = 6.095.. 6개의 WG을 올릴 수 있다.


8kB sGPR : 4 * 2K

순서대로 4byte, 2K sGPR을 의미한다

  • 즉 CU에서 최대로 사용할 수 있는 sGPR이 2K 개로 만약 WG이 4WF로 이루어져있고 WF당 50 sGPR를 필요로 한다면 WG당 200 sGPR을 필요로 한다. 따라서 10.24, 10개의 WG가 CU에 올라갈 수 있다.


64kB LDS

  • 간단하게 하나의 WG에 4kB의 local memory를 할당해 준다면 64 / 4 = 16, 즉 16WG를 올릴 수 있다.


결과적으로 위 4개의 제약사항을 모두 만족할 수 있는 WG가 CU에 올라가게 된다. 따라서 occupancy는 다음과 같다