2019.04.01 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. 자료구조의 큰 틀에 대해 이야기하기

자료구조

자료구조는 메모리에 data를 어떻게 저장할 것인가에 대한 고민부터 시작된다.
list, tuple, dictionary 모두 자료구조의 한 형태이다

자료구조는 딱 3가지이다.

  1. 데이터를 어떻게 삽입할 것인가?(insert)
  2. 데이터를 어떻게 검색할 것인가?(search)
  3. 데이터를 어떻게 삭제할 것인가?(delete)

ADT(abstract data type) : 추상 자료형

자료 구조에서 삽입, 탐색, 삭제 등을 담당하는 함수들의 사용 설명서

  1. 어떤 자료구조가 가지고 있는 오퍼레이션(함수)의 나열(목록)
  2. 함수 시그니처(인터페이스)만 나열할 뿐, 내부 구현은 표기하지 않음
  3. 함수(operation)의 작동 방식 설명.

ex)

>>>    help(list.append)
help on method_descriptor:

append(...)
    L.append(object) -> None
>>>

우리는 여기에서 append함수가 어떻게 구현되었는지 알 수 없다. 하지만 append함수를 사용하는 것에는 전혀 문제가 없다. 이러한 특징을 인터페이스와 구현이 분리되었다고 하며 추상화라는 표현을 쓴다.

linked List

데이터와 참조로 구성된 노드가 한 방향 혹은 양방향으로 쭉 이어져 있는 자료구조

  • single linked list
  • double linked list

노드

자료구조를 구현할 때 데이터를 담는 틀

node
  • 노드는 저장할 데이터와 다음 노드를 가르키는 참조로 이루어져 있다.(단일 연결리스트)
  • double linked list에서는 다른 형태의 노드가 활용된다.

single linked list

single linked list에서의 노드 구현

class Node:
    def __init__(self, data=None):
        self.__data = data
        self.__next = None


# 소멸자
# 객체가 사라지기 전에 반드시 한번 호출하는 것을 보장한다.
# 소멸자를 호출하고 지우는 것을 보장해준다.
# 실제 로드가 사라지는 것을 확인하기 위해서 가지고 옴
    def __del__(self):
        print(f'node[{self.__data}] deleted!')
        pass

    @property
    def data(self): 
        return self.__data

    @data.setter
    def data(self, data):
        self.__data = data

    @property
    def next(self):
        return self.__next

    @link.setter
    def next(self, next):
        self.__next = next
  • 자료구조에서는 보안이 중요하므로 정보은닉 2가지를 모두 적용해주었다.
  • 멤버 __data에는 데이터를 저장하고, 멤버 __ next는 다음 노드를 가르킨다.

single linked list의 인스턴스 멤버

  1. head -> 리스트의 첫 번째 노드를 가르킨다.
  2. d_size -> 리스트의 요소 개수이다.

single linked list의 ADT

  1. S.empty() ---> Boolean

    • 리스트가 비었으면 True, 아니면 False
  2. S.size() ---> integer

    • 리스트에 있는 요소 개수
  3. S.add() ---> None

    • 노드를 리스트의 맨 앞에 추가
  4. S.search(target) ---> node

    • 리스트에서 target을 찾는다.
    • 찾으면 노드를, 못 찾으면 None 반환
  5. S.delete() ---> None

    • 맨 앞의 노드 삭제

single linked list 구현


class SLinkedlist:

    def __init__(self):
        self.head = None
        self.d_size = 0

    def empty(self):
        if self.d_size == 0:
            return True
        else:
            return False

    def size(self):
        return self.d_size

    def add(self, data):
        new_data = Node(data)
        new_data.next = self.head
        self.head = new_data
        self.d_size += 1

    def search(self, target):
        cur = self.head
        while != None:
            if cur.data == target:
                print(f"찾으신 데이터:{cur.data}가 있습니다")
                return cur
            cur = cur.next
        return None

    def delete(self):
        self.head = self.head.next

def show_list(sll):
    cur = sll.head
    for _ in range(sll.size()):
        print(cur.data, end=" ")
        cur = cur.next

if __name__ == "__main__"
    single = SLinkedlist()
    single.add(1)
    single.add(2)
    single.add(3)
    single.add(4)
    single.add(5)

    print(single.size())
    single.search(3)
    show_list(single)

    single.delete()
    show_list(single)

5
찾으신 데이터 3가 있습니다.
1 2 3 4 5
node[5] deleted!
2 3 4 5
node[4] deleted!
node[3] deleted!
node[2] deleted!
node[1] deleted!

Dummy double linked list

  • dummy

    • 더미란 데이터를 가지지 않는 노드를 의미한다.
    • double linked list에서는 양 옆에 배치한다.
  • 구현 이유

    • 더미가 있게 됨으로서 데이터가 있던지 없던지 상관없이 간단하게 데이터를 추가 삭제할 수 있게 되었다.원래는 뒤에 데이터가 있는지 앞에 데이터가 있는지 확인을 해야하지만 더미(head와 tail)이 있게 되므로서 상관하지 않고 구현할 수 있다.

double linked list 노드

노드1

Dummy double linked list instance Member

  • instance member

    • head : 리스트 맨 앞에 있는 더미를 가르킨다.
    • tail : 리스트 맨 뒤에 있는 더미를 가르킨다.
    • d_size : 리스트의 요소 개수
  • ADT

double linked list 1 double linked list 2

Dummy double linked list 구현

class Node:
    def __init__(self, data=None):
        self.__data = data
        self.__prev = None
        self.__next = None
        print(self)

    def __del__(self):
        print("data of {} is deleted".format(self.data))

    @property
    def data(self):
        return self.__data

    @data.setter
    def data(self, data):
        self.__data = data

    @property
    def prev(self):
        return self.__prev

    @prev.setter
    def prev(self, b):
        self.__prev = b

    @property
    def next(self):
        return self.__next

    @next.setter
    def next(self, n):
        self.__next = n

    def __str__(self):
        return f"created:[{self.__data}]"


class DoubleLinkedList:

    def __init__(self):
        self.head = Node()  
        self.tail = Node()
        self.d_size = 0

        self.head.next = self.tail
        self.tail.prev = self.head

    def empty(self):
        if self.d_size == 0:
            return True
        else:
            return False

    def size(self):
        return self.d_size

    def add_first(self, data):  
        new_node = Node(data)  
        new_node.next = self.head.next
        new_node.prev = self.head
        self.head.next.prev = new_node
        self.head.next = new_node
        self.d_size += 1


    def add_last(self, data):  # 항상 제일 앞에는 first가 있다고 가정한다.
        new_node = Node(data)
        new_node.next = self.tail
        new_node.prev = self.tail.prev
        self.tail.prev.next = new_node
        self.tail.prev = new_node
        self.d_size += 1

    def insert_after(self, data, node):
        new_node = Node(data)            
        new_node.next = node.next
        new_node.prev = node
        node.next.prev = new_node
        node.next = new_node
        self.d_size += 1

    def insert_before(self, data, node):
        new_node = Node(data)
        new_node.next = node
        new_node.prev = node.prev
        node.prev.next = new_node
        node.prev = new_node
        self.d_size += 1

    def search_forward(self, target):
        cur = self.head.next
        while cur is not self.tail:
            if cur.data == target:
                return cur
            cur = cur.next
        return None  

    def search_backward(self, target):
        cur = self.tail.prev
        while cur is not self.head:
            if cur.data == target:
                return cur
            cur = cur.prev
        return None


    def delete_first(self):
        if self.empty():
            return
        else:        
            self.head.next = self.head.next.next
            self.head.next.prev = self.head

        self.d_size -= 1

    def delete_last(self):
        if self.d_size == 0:
            return

        self.tail.prev = self.tail.prev.prev
        self.tail.prev.next = self.tail
        self.d_size -= 1

    def delete_node(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

        self.d_size -= 1

    # 편의 함수
    def traverse(self, start=True):
        if start:
            # 리스트의 첫 데이터부터 순회를 시작
            cur = self.head.next
            while cur is not self.tail:  # is는 객체 자체의 비교이다.
                yield cur
                cur = cur.next

        else:
            # 리스트의 마지막 데이터부터 순회
            cur = self.tail.prev
            while cur is not self.head:
                yield cur  # 만약에 아래 쪽에 node.data를 안했으면 cur.data해야 한다.
                cur = cur.prev


def show_list(d_list):
    g = dlist.traverse()
    for node in g:
        print(node.data, end=" ")
    print()


if __name__ == "__main__":
    dlist = DoubleLinkedList()
    dlist.add_first(1)
    dlist.add_first(2)
    dlist.add_first(3)
    dlist.add_first(4)
    dlist.add_first(5)

    show_list(dlist)

    # 데이터를 찾은 이유는 그 뒤에 데이터를 넣기 위해서
    searched_data = dlist.search_backward(3)
    if searched_data:
        # dlist.delete_node(searched_data)
        dlist.insert_before(15, searched_data)
        print(f"searched data:{searched_data.data}")
    else:
        print('기준 노드가 없습니다. 다시 확인해주세요.')

    show_list(dlist)
    # dlist.delete_first()
    # dlist.delete_last()
    del_data = dlist.search_forward(15)
    if del_data:
        # del_data가 15를 가르키고 있으므로 레퍼런스 카운터가 0이 안된다. 따라서 None을 해서 레퍼런스 카운터를 0으로 만든다.
        dlist.delete_node(del_data)
        del_data = None
    else:
        print("기준 노드가 없습니다. 다시 확인해주세요")
    show_list(dlist)
    print("*" * 100)
    # generator 객체


created:[None]
created:[None]
created:[1]
created:[2]
created:[3]
created:[4]
created:[5]
5 4 3 2 1 
created:[15]
searched data:3
5 4 15 3 2 1 
data of 15 is deleted
5 4 3 2 1 
****************************************************************************************************
data of None is deleted
data of 1 is deleted
data of 2 is deleted
data of 3 is deleted
data of 4 is deleted
data of 5 is deleted
data of None is deleted
  • self를 앞에 붙이는 것은 무언가 지속적으로 남겨놓을 때 붙인다.
  • 따라서 지속적으로 남겨놓을 필요가 없을 때는 붙이지 않는다.

2019.03.28 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. Stack 2개로 queue 구현하기

Stack와 Queue

  • Stack과 Queue는 모두 컴퓨터 자료구조 방법론에 해당한다.

  • Stack : LIFO (last in first out) 후입 선출

  • Stack의 ADT (operation list , operation의 시그니처만 나열)

    1. S.empty() --> Boolean (스택이 비어있으면 True, 아니면 False)
    2. S.push(data) ---> None (스택의 맨 위에 데이터를 쌓는다.) None->무조건들어간다
    3. S.pop() ---> data (스택 맨 위의 데이터를 삭제하면서 반환)
    4. S.peek() ---> data (스택 맨 위의 데이터를 반환)
  • Queue : FIFO (first in first out) 선입 선출

  • Queue의 ADT (operation list , operation의 시그니처만 나열)

    1. Q.empty() —> Boolean (큐가 비어있으면 true, 비어있으면 False)
    2. Q.enqueue(data) —> None (큐의 맨 뒤에 데이터를 쌓는다.)
    3. Q.dequeue(data) —> data (큐의 맨 앞에 데이터를 삭제하면서 반환)
    4. Q.peek() —> data (큐의 맨 앞의 데이터를 반환)

Stack의 ADT 구현

  • ADT 구현
>>>class Stack:
>>>    def __init__(self):
>>>        self.container=list()
>>>
>>>    def empty(self):
>>>        if not self.container:
>>>            return True
>>>        else:
>>>            return False
>>>
>>>    def push(self, data):
>>>        self.container.append(data)
>>>
>>>    def pop(self):
>>>        return self.container.pop()
>>>
>>>    def peek(self):
>>>        return self.container[-1]

위와 같이 스택의 ADT를 표시할 수 있다.

Queue의 ADT 구현

  • ADT 구현
>>>class Queue:
>>>
>>>    def __init__(self):
>>>        self.container = list() 
>>>    
>>>    def empty(self):
>>>        if not self.container:
>>>            return True
>>>        else:
>>>            return False
>>>    def enqueue(self, data):
>>>        self.container.append(data)
>>>
>>>    def dequeue(self):     
>>>        return self.container.pop(0)
>>>
>>>    def peek(self):
>>>        return self.container[0]

위와 같이 Queue의 ADT를 표시할 수 있다.

Stack 2개로 Queue 구현하기

  • 2개의 Stack을 활용하여 Queue의 ADT를 구현해야 한다.
  • Stack의 ADT를 활용할 수 있다.
  • 1개의 자료는 Stack을 Stack1 --> Stack2로 한번만 이동가능하다.
  • 같은 폴더 안에 Stack의 ADT가 저장되어 있다고 가정(import 사용)

실제 구현

>>> from stack import Stack 
>>>
>>> class Queue:
>>>     
>>>        def __init__(self):
>>>            self.stack1 = Stack()
>>>            self.stack2 = Stack()
>>>
>>>        def empty(self):
>>>            if self.stack1.empty() and self.stack2.empty():
>>>                return True
>>>            else:
>>>                return False
>>>
>>>        def enqueue(self, data):
>>>            self.stack1.push(data)
>>>
>>>        def dequeue(self):
>>>            if self.stack2.empty():
>>>                while not self.stack1.empty():
>>>                    self.stack2.append(stack1.pop())
>>>            return self.stack2.pop()
>>>
>>>        def peek(self):
>>>            if self.stack2.empty():
>>>                while not self.stack1.empty():
>>>                    self.stack2.append(self.stack1.pop())
>>>            return self.stack2.peek()
>>>
>>>
>>> if __name__ == "__main__":
>>>        Que = Queue()
>>>        for i in range(10):
>>>            Que.enqueue(i)        
>>>        
>>>        print(Que.peek())
>>>        print(Que.dequeue())
>>>        print(Que.dequeue())
>>>        Print(Que.empty())
>>>        print(Que.enqueue(10))
>>>        print(Que.dequeue())
>>>        
0
0
1
False
None
2

이해하기가 쉽지 않다면 유튜브 영상을 추천한다.

[추천 참고 영상] (https://youtu.be/t45d7CgDaDM)

2019.03.14 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

자료구조 Stack & Queue

1.Stack

stack이란 무언가를 쌓는다라는 의미를 갖는 자료구조이다.
stack의 사전적의미를 찾아보면
: [명사] 동적이고 순차적인 자료의 목록.
: [영어] 무더기, 많음 다량, 굴뚝.
이라고 나온다.

LIFO(Last in, First out)

후입선출.
스택은 접시를 쌓는 것과 같기 때문에 먼저 들어간 자료보다는 가장 늦게 쌓이고 가장 위에 있는 접시가 먼저 나오게 된다.
이것은 후입선출이라고 한다.

먼저 스택을 사용하기 위해서 ADT라는 개념을 배워보자.

ADT(abstract data type) : 추상 자료형

  1. 어떤 자료구조가 가지고 있는 오퍼레이션(함수)의 나열(목록)
  2. 함수 시그니처(인터페이스)만 나열할 뿐, 내부 구현은 표기하지 않음
  3. 함수(operation)의 작동 방식 설명.

위의 조건을 꼭 지켜줘야한다. ADT에 대해 명쾌히 설명을 해준 블로그를 참고해보면,

추상 자료형(Abstract Data Type)

기능의 구현 부분을 나타내지 않고 순수한 기능이 무엇인지 나열한 것을 추상 자료형이라고 한다.

예를 들면, 사용 설명서와 같다. 선풍기의 사용 설명서를 본다고 가정하자. 사용 설명서에는 정지, 미풍, 약풍, 강풍, 회전, 타이머등의 기능 설명과 사용 방법이 나와있다. 하지만 버튼을 눌렀을 때 선풍기 내부회로에서 어떤 일이 발생하는지에 대해서는 전혀 나와있지 않다. 추상 자료형은 선풍기의 사용 설명서와 같이 기능과 사용 방법을 정의한 것이다.

추상 자료형의 필요성

추상 자료형은 구현자와 사용자를 분리해 준다. 라이브러리를 가져다 쓰거나 내장 함수를 사용하는 것도 추상 자료형이 정의되어 있기 때문이다. 또한 추상 자료형에 대한 구현은 외부로 부터 숨겨져 정보 은닉(Information Hiding)이 이루어지게 된다.

추상 자료형 정의

무선 마우스의 추상 자료형을 정의해 본다면,

ADT of Wireless Mouse

  • Data

: Power, LeftButton, RightButton 전원, 왼쪽버튼, 오른쪽버튼

  • Operation

: PowerOn() 전원 On

: PowerOff() 전원 Off

: LeftButtonClick() 왼쪽 버튼 클릭

: RightButtonClick() 오른쪽 버튼 클릭

출처: https://ledgku.tistory.com/41 [견우와 직녀]

위의 글을 참조하여 한번에 개념을 이해하였다.

선생님은 코드를 통해 ADT에 대해 이야기해주셨다.

>>>dir(list)
>>>
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__delitem__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__iadd__',
 '__imul__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__reversed__',
 '__rmul__',
 '__setattr__',
 '__setitem__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'append',
 'clear',
 'copy',
 'count',
 'extend',
 'index',
 'insert',
 'pop',
 'remove',
 'reverse',
 'sort']

위와 같이 list라는 객체의 내장 메소드들이 나오게 되는데
(#_method_ 와 아래 나온 메소드들의 차이를 알아보자!)

여기서 아래에 나오는 append, clear, copy와 같은 메소드들을
다시 코드를 통해 검색해보면

>>>help(list.append)
>>>Help on method_descriptor:

append(...)
    L.append(object) -> None -- append object to end

위와 같은 것을 얻을 수 있다. 즉 ADT가 요구하는 3가지 조건에 부합하는
설명을 볼 수 있다.
함수의 작동방식에 대해 설명해주고 있고(help on method_descriptor)
함수의 시그니처(인터페이스)만 설명하고 내부구현에 대해서는 언급하지 않으므로 추상자료형의 표기법으로 적합하다.

위와 같은 표기법으로 append, clear, copy, count, extend,,, sort까지 모아놓으면 그게 list의 ADT이다

Stack의 ADT

Stack의 ADT (operation list , operation의 시그니처만 나열)

  1. S.empty() --> Boolean (스택이 비어있으면 True, 아니면 False)
  2. S.push(data) ---> None (스택의 맨 위에 데이터를 쌓는다.) None->무조건들어간다
  3. S.pop() ---> data (스택 맨 위의 데이터를 삭제하면서 반환)
  4. S.peek() ---> data (스택 맨 위의 데이터를 반환)
  • ADT 구현
>>>class Stack:
>>>    def __init__(self):
>>>        self.container=list()
>>>
>>>    def empty(self):
>>>        if not self.container:
>>>            return True
>>>        else:
>>>            return False
>>>
>>>    def push(self, data):
>>>        self.container.append(data)
>>>
>>>    def pop(self):
>>>        return self.container.pop()
>>>
>>>    def peek(self):
>>>        return self.container[-1]

위와 같이 스택의 ADT를 표시할 수 있다.

코드를 통한 Stack

스택을 사용하기 위해서는 stack 모듈을 불러와야 한다.

>>> from stack import Stack
>>> st=stack()       #스택이 만들어진다. list와 비슷하다.
>>> st.push(1)
>>> st.push(2)
>>> st.push(3)
>>> st.push(4)
>>> st.push(5)
>>> while not st.empty():
>>>     data = st.pop())
>>>     print(data)
>>> 5
>>> 4
>>> 3
>>> 2
>>> 1
>>> 

Stack과 함께 또 봐야하는 것이 있는데 바로 queue이다.

Queue(큐)

지금 놓치지 말아야 할 것은 stack와 queue는 모두 자료구조에서 자료가 저장되는 형태를 나타는 것이다.
queue의 사전적의미를 보면

: 규, 대기 행렬, 줄을 서서 기다리다
: 꼬리, 끝, 후미.

를 말한다. stack과는 다른 형태로 자료구조가 쌓인다.
Stack의 단점은 늦게 온 사람이 먼저 일을 처리하는 형태로 먼저 온 사람은 지속적으로 기다려야 하는 단점이 있다.
그에 비해 우리가 일반적으로 줄서기와 같은 것이 queue인데
먼저 기다린 사람이 먼저 나간다. FIFO(First in First Out) : 선입선출이며 보통 프로세스 처리, cpu관리에서 많이 쓰인다.

큐의 문제점은 일반적으로 배열을
[1][2][3][4][5]
이런 직선 형태로 보왔을 때,
[Pop][2][3][4][5] 가장 오래기다린, 처음 들어온 [1]이라는 데이터가 Pop이 되면 다른 데이터들을 차례대로
땡겨주어야 한다.
( 소수의 자료의 경우는 상관이 없지만 많은 데이터의 경우 연산에 많은 시간이 걸린다. )

이러한 문제점을 해결하기 위해 나온게 원형 큐, 순환 큐, 환영 큐 라고 불리는 방벙인데 배열을 직선이 아니라 원형으로 보는 것이다. (#다음에 추가적인 공부가 필요하다)

Queue의 ADT

Queue의 ADT (operation list , operation의 시그니처만 나열)

  1. Q.empty() —> Boolean
    1. 큐가 비어있으면 true, 비어있으면 False
  2. Q.enqueue(data) —> None
    1. 큐의 맨 뒤에 데이터를 쌓는다.
  3. Q.dequeue(data) —> data
    1. 큐의 맨 앞에 데이터를 삭제하면서 반환
  4. Q.peek() —> data
    1. 큐의 맨 앞의 데이터를 반환
  • ADT 구현
>>>class Queue:
>>>
>>>    def __init__(self):
>>>        self.container = list() 
>>>    
>>>    def empty(self):
>>>        if not self.container:
>>>            return True
>>>        else:
>>>            return False
>>>    def enqueue(self, data):
>>>        self.container.append(data)
>>>
>>>    def dequeue(self):     
>>>        return self.container.pop(0)
>>>
>>>    def peek(self):
>>>        return self.container[0]

위와 같이 Queue의 ADT를 표시할 수 있다.

위에서 배운 것을 바탕으로 다음에는 Stack을 활용하여 계산기를 구현한다!

2019.03.11 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. linear_search

li라는 list에서 특정한 target의 인덱스를 찾으려면 어떻게 해야할까?
li = [ 5, 7, 2, 8, 3, 9, 1] 이라는 것에서
3이라는 target의 인덱스를 찾을 수 있는 함수를 구현해보자.

linear_search(li, target)--> idx
반환값은 target이 있다면 target의 인덱스
target이 없다면 None을 반환

def linear_search(li, target):
        for i in len(li):
            if li[i] == target:
                return i
        return None      # target이 없으면  None을 반환해라.

굉장히 간단한 함수이다. 하지만 함수의 성능을 보면 굉장히 비효율적으로 볼 수 있다.

알고리즘의 성능차이 비교 절대 시간을 비교 하면 안된다.
컴퓨터 성능이 차이나므로 상대 시간 : 어떤 알고리즘을 실행하는데 해비한 연산의 연산 횟수를 비교하자.
즉 데이터의 갯수에 따른 연산 횟수를 계산해서 성능을 계산하자

이 연산에 + - 등 보다는 그 알고리즘을 구성하는데 가장 헤비하게 미치는 연산은 for문이다.
일반적으로 for문 혹은 if문 비교 연산 if문을 많이 호출하면 느린거고 if문을 조금 호출하면 빠른 것이다.

또한 데이터가 늘어남에 따라 연산횟수가 얼마나 변화하는지를 비교해 봐야 한다.

성능을 평가할떄는 3가지 방법이 있다.

  1. 최선의 경우
  2. 평균의 경우 --> quick sort
  3. 최악의 경우 --> 알고리즘을 말할때는 항상 최악의 경우를 가정한다.
    • 이 알고리즘은 아무리 최악의 경우 몇회의 연산횟수를 보장한다.

성능 평가에 대해 조금 더 공부해보려고 한다.

What does T(n) mean in relation to O(n)?

When discussing an algorithm, the common usage of the symbol 𝑇T is to represent its time complexity.
𝑇T is a function. The input to this function is the input size, the output is the (worst-case) running time for an input of that size.
Therefore, 𝑇(𝑛)T(n) is a number: the number returned by the function 𝑇T when given the number 𝑛n. This number is the running time of our algorithm on an input of size 𝑛n.
The 𝑂O in "𝑂(𝑛)O(n)" is not a function. This is Big O notation. In particular, the symbol 𝑂(𝑛)O(n) represents the class of all functions that grow (asymptotically) at most as quickly as the linear function 𝑓(𝑛)=𝑛f(n)=n.
Big O notation gives us a convenient way to talk about upper bounds. For example, we can say "the time complexity of this algorithm is 𝑂(𝑛2)O(n2)" (formally: "𝑇∈𝑂(𝑛2)T∈O(n2)") to say that the running time of the algorithm is at most quadratic in the input size.
Sometimes, you can see both symbols being used in a single equation. For example, the time complexity of MergeSort is commonly described by the following recurrence: "𝑇(𝑛)=2𝑇(𝑛/2)+𝑂(𝑛)T(n)=2T(n/2)+O(n)".
The meaning of the above statement: For any 𝑛n, the time 𝑇(𝑛)T(n) needed to sort 𝑛n elements can be computed by taking the time 𝑇(𝑛/2)T(n/2) needed to sort 𝑛/2n/2 elements, multiplying that time by 2, and adding something extra. That something extra must be at most linear in 𝑛n.

What does T(n) mean in relation to O(n)? - Quora

해석 :

알고리즘을 논할 때, 기호 T의 일반적인 용도는 시간 복잡성을 나타내는 것이다.

T는 함수다. 이 함수에 대한 입력은 입력 크기, 출력은 해당 크기의 입력에 대한 (최악의) 실행 시간이다.

따라서, T(n)는 숫자다: n을 부여했을 때 T 함수가 반환하는 숫자다. 이 숫자는 크기 n의 입력에 대한 우리의 알고리즘의 실행시간이다.

"O(n)"의 O는 기능이 아니다. 이것은 Big O 표기법이다. 특히 기호 O(n)는 선형 함수 f(n)=n만큼 빠르게(증식적으로) 성장하는 모든 기능의 클래스를 나타낸다.

Big O 표기법은 우리에게 상위에 대해 이야기할 수 있는 편리한 방법을 제공한다. 예를 들어, 우리는 "이 알고리즘의 시간 복잡성은 O(n2)"(공식적으로 "TOO(n2))라고 말할 수 있다.

때때로, 당신은 하나의 방정식에 두 개의 기호가 사용되는 것을 볼 수 있다. 예를 들어, MergeSort의 시간 복잡성은 일반적으로 "T(n)=2T(n/2)+O(n)"에 의해 설명된다.

상기 문장의 의미 : 어떤 n에 대해서도 n 요소를 분류하는데 필요한 T(n/2)는 n/2 요소를 분류하는 데 필요한 시간을 취하고 그 시간을 2로 곱한 후, 어떤 것을 추가함으로써 계산할 수 있다. 그 어떤 특별한 것은 n에 가장 직선적일 것이다.

---->> T 함수는 함수에 대한 최악의 실행시간이다. T(n)은 숫자인데 n 크기에 대한 자료를 함수에 넣었을 때 걸리는 실행시간이다.

---->> 우리가 말하는 알고리즘의 성능이 어떤 것인지 T of N 을 항상 구할 수는 없으므로 big O를 구하는 것을 배운다.

binary_search

def binary_search(li, target):
    """
    binary_search(li, target)--> idx
    반환값은 target이 있다면 target의 인덱스
    target이 없다면 None을 반환
    """
binary search

binary_search는 기본적으로 들어오는 자료가 정렬이 되었다고 가정하고 시작한다.
이를 통해 우리는 정렬된 자료의 중간값과 target을 비교하고, target이 중간값보다 작다면
중간값 이후의 배열에는 신경쓰지 않고 중간값이 전의 값들로만 비교하겠다는 것이다.
이를 통해 우리는 for 문을 돌렸을 때보다 훨씬 좋은 성능의 알고리즘을 구현할 수 있다.
(실제로도 많이 쓰이는 알고리즘이다)

구현

def binary_search(li, target):
        start = 0       #(인덱스는 0부터 시작하므로)
        end = len(li) - 1       #(인덱스를 기준으로 해야함으로 1을 빼줘야 한다)
        while start <= end:
                mid = (start + end) // 2
                if li(mid) > target:
                    end = mid - 1
                elif li(mid) < target:
                    start = mid + 1
                else:
                    return mid   # 이러면 실제로 리턴 값에만 들어가게 되므로 시스템적으로 나오는 것은 없다. 따라서 꼭 print를 해서 화면에 나오게 해줘야 한다. or 이 함수를 다른 변수에 할당하고 그 변수를 print하도록 한다.
        return None

[
참고자료] (https://github.com/fabl1106/CS/blob/master/data_structure/binary_search/binary_search.pdf)

새롭게 알게 된 중요한 사실

def exam():
        return 1

exam() 

을 실행해보면 아무것도 나오지 않는다. 숫자 혹은 string을 적어도 마찬가지 이다.

여기서 중요하게 알아야 할 것은 return을 해주는 것은 그 함수에서 리턴 값을 거기에 할당해주는 것이기 때문에 화면에는 나타나지 않는다.
이것을 해결하기 위해서는 return 위에 print 를 적어 화면에 표시하고 싶은 것을 적고 아래에
return 만을 적어서 함수를 빠져나오게 하거나
exam()을 특정변수에 할당해주어서 ( a = exam() ) 그 변수를 print 하는 방법이 있다.

다시 방법을 적어보면

def exam():
        print(1)
        return
exam()        # 이렇게 하면 return 값은 None이 반환된다. 따라서 상황에 따라 return 뒤에 1을 적어줘야 한다.

혹은

def exam():
        return 1

a = exam()
print(a)     #print를 해주어야 한다. 정 위와 같이 해주고 싶다고 하면 exam() 앞에 print를 해주면 된다.

def exam():
        return 1
print(exam())

2019.04.06 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. 네트워크의 큰 그림에 대해 이야기 해보기

들어가기

나만 가지고 있는 데이터는 크게 의미가 없다.
우리는 서로의 데이터를 공유할 때 그 효과는 극대화 된다.

이러한 것을 실현 시켜주는 것이 네트워크이다. net + work 라는 글에서 알 수 있듯이
우리가 가진 데이터들을 공유시킬 수 있는 망이다.

인터넷은 international network 라는 말의 줄인말이며 네트워크의 네트워크라고 할 수 있다.

이렇게 규모가 커진 인터넷을 관리하기 위해서는 여러가지 규약들이 필요하게 되었다.
즉 network간의 데이터를 교환하기 위한 규약이 생기게 된 것이다. 이를 프로토콜이라고 한다.

이 프토토콜을 기능별로 나누어 OSI 7계층 혹은 TCP/IP로 부른다.
이렇게 계층화를 시킴으로서 통신에 문제가 발생하였을 때 어디서 발생했는지 찾기가 쉬워지고,
데이터의 흐름을 알 수 있게 되었다.

TCP/IP 4계층으로 네트워크의 큰 그림을 그려보고자 한다.
기본적으로 우리는 client / server 그리고 중간에서 통신을 도와주는 라우터로 나눌 수 있다.

네트워크

network

위의 그림은 내가 일단 네트워크를 정리한 큰 그림이다.(4계층으로 정리)

각각의 상세 기능에 대한 설명은 뒤로 하고 이번에는 먼저 큰 것들에 대해 설명해보고자 한다.

network interface(파랑)

먼저 client(우리 노트북)에서 우리가 속해 있는 LAN(local area network) 범위 즉 라우터 (IPTime 으로 생각해도 좋다)까지 먼저 데이터를 전송해야 한다. 그 가장 기본이 되는 것이 네트워크 인터페이스(파란색)이다.

네트워크 인터페이스에서 NIC(network interface controller)는 컴퓨터에 설치되어 네트워크와 연결하기 위한 장치로 흔히 우리가 말하는 랜카드이다. 그리고 이 NIC가 가지고 있는 하드웨어 주소를 MAC주소이라고 한다. 이런 LAN사이의 통신은 이더넷프로토콜이 책임이며 MTU(maximum transmission unit)는 한번에 보낼 수 있는 최대 데이터 양으로 약 1500byte정도 된다.

internet(빨강)

다음으로는 우리가 많이 부르는 IP에 관한 내용이다. 각각의 라우터는 ISP(internet service provider)로 부터 공인 IP를 부여받는다. 이 IP는 ipv4 버전으로 4byte로 표현된다.(2진법) 이것을 10진법으로 바꾸어 192.168.142.68 처럼 IP주소가 된다. 라우터를 통해 라우터에 접속되어 있는 client들은 사설 IP를 부여 받게 되고 각각의 client들의 사설 IP와 MAC Address를 일치 시키기 위해 ARP(address resoultion protocol)을 따른다. 이는 브로드캐스팅(모두에서 ping을 쏘아 회신을 받는다) 방법을 통해 접속해 있는 모든 client들의 MAC주소를 사설 IP주소와 묶어 하나의 table로 만든다.
그리고 좀 더 효율적인 관리를 위해 서브넷 마스크를 도입하여 IP의 낭비를 막는다.
또한 client의 사설 IP주소가 라우터로 왔을 때 사설 IP주소로는 그 이후로는 더 전달할 수 없으므로 이 사설 IP주소와 라우터가 가지고 있는 공인 IP주소를 바인드하는데 이를 NAT(network address translation)라고 한다. 하지만 하나의 공인 IP주소와 하나의 사설 IP주소를 바인딩하는 것은 1개 혹은 2개 밖에 바인딩하지 못해 다른 client들의 접근이 제한되므로 NART(network address port translation)방법을 통해 각각의 IP주소 뒤에 port번호를 추가한다.
보통 미국에 있는 서버에 접속하기까지 10개 이상의 라우터들을 거쳐야 하는데 어떤 라우터들을 거쳐서 갈지를 결정하는 알고리즘이 IP routing이라고 한다.(router는 hop이라고도 부른다) 한번 다녀간 다음에는 route table에 맵핑을 해놓는다.

transport(초록)

client에서 라우터 혹은 라우터에서 서버로 접속할 때는 데이터의 transport가 일어나게 된다. 데이터 전송법은 크게 2가지가 존재한다. 한개는 TCP(transmission control protocol) 또 한개는 UDP(user datagram protocol)이다.
이 TCP는 신뢰성에 집중하여 유실 될수도 있는 부분을 검출 해내어 재전송하고 순차적으로 전송하며(따라서 느리다), UDP는 신뢰성보다는 빠른 전송에 집중하여 데이터를 전송한다. 보통 스트리밍 혹은 게임등에 데이터 전송에 사용된다.
TCP는 먼저 서로의 연결을 완료한 뒤( three way handshaking 방법 사용) 연결이 끝나면 데이터를 전송한다.(sliding - window 방법 사용)

application(분홍)

그럼 이러한 모든 과정이 일어나는 곳은 어디일까? 바로 application이다. application을 통해 우리는 직접적으로 상호작용한다. 우리가 자주 이용하는 크롬 혹은 익스플로러가 될 수도 있고, HTTP, SSH, DNS 등이 될 수도 있다.

전체적인 그림을 나름 많은 자료를 바탕으로 그려보았으나, 이렇게 설명해놓은 곳은 없어서 걱정스럽기도 하다. 하지만 일단 지식을 내 머리속에 넣어야 하기 때문에 하나의 틀로 만들어보았다. 앞으로 공부해가며 틀린 부분이 있으며 수정해야겠다. 추가적으로 각 내용들의 상세부분은 따로 한번 더 정리할 예정이다.

2019.04.03 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  • 우리가 유튜브를 볼 때 일어나는 일들에 대해 최대한 상세하게 이야기 해보기

네트워크의 시작

  • 네트워크란 : 노드(데이터를 저장하는 공간)들이 서로를 공유할 수 있게 하는 통신망
  1. 컴퓨터 네트워크의 등장

    1. 중앙 집중형: 하나의 노드가 파괴되면 한번에 파괴될 수 있음
    2. 분산형 : 중앙 집중형의 문제 해결
  2. 패킷 통신 방식의 발달

    1. 데이터를 패킷 형태로 나누어서 전송 : 목적지에서 다시 패킹 작업을 해야함
    2. 네트워크 트래픽을 여러개의 컴퓨터가 공유할 수 있으므로 장점
  • 분산 네트워크 + 패킷 통신 방식 ==> 인터넷의 기반
  • 각각의 네트워크가 연결되며 인터넷의 구조를 가지게 되었다.
  • 인터넷은 네트워크의 네트워크이다.

거대한 인터넷을 연결하고 데이터를 주고 받기 위한 규약이 필요해지게 되었다.

분산 네트워크 + 패킷 통신 방식

  1. 네트워크 구조를 유지하기 위한 프로토콜 : IP(internet Protocol)

    1. 네트워크에 참여한 노드를 유일하게 확인해준다.
    2. 전체 네트워크에 식별 가능한 IP주소를 부여
    3. 노드와 노드 사이에서 경로 설정을 위한 라우터가 있음
  2. 통신을 보장하기 위한 프로토콜 : TCP(transmission control protocol)

    1. 데이터는 여러 패킷조각으로 나누어 이동
    2. 패킷은 순서대로 오지 않을 수 있고, 훼손될 수 있다.
      1. TCP는 패킷을 재 조립하고 재 전송을 요청하는 등 흐름을 관리한다.

OSI 7계층과 TCP/IP

  • 인터넷 응용 개발을 위해 네트워크를 7계층으로 나눔
  • OSI 모형(Open Systems Interconnection Reference Model)은 국제표준화기구(ISO)에서 개발한 모델로, 컴퓨터 네트워크 프로토콜 디자인과 통신을 계층으로 나누어 설명한 것이다. 일반적으로OSI 7 계층 모형이라고 한다.
  • OSI 7계층을 통합하여 TCP/IP 4계층으로 통합

TCP:IP

OSI 7계층에서 꼭 알아야 하는 것들

  • 응용 계층(Application Layer) : FTP, HTTP, SSH
  • 표현 계층(Presentation Layer)
  • 세션 계층(Session Layer)
  • 전송 계층(Transport Layer) : TCP, UDP
  • 네트워크 계층( Network Layer) : IP
  • 데이터 링크 계층(Data Link Layer) : Ethernet, NIC
  • 물리 계층(Physical Layer)

Network interface(TCP/IP) : MAC Address를 다룬다.

  • Network interface는 컴퓨터에서 internet(IP Address)까지 가는 것을 다룬다.(LAN)
  • 물리 계층(physical Layer)
  • 데이터 링크 계층(data link Layer)

Physical Layer

  • 컴퓨터 2대를 연결하는 것
  • 서로의 컴퓨터에 LAN 케이블을 연결하여 스타크래프트 접속
  • 눈에 보이는 것을 이야기한다.
  • 랜케이블
  • IPTIME
    • 라우터 기능 :
    • 스위칭 기능 : twist가 되도록 해준다. 2개의 랜을 연결하면 알아서 스위치 해준다.
    • 리피터 기능 : 거리가 멀어지면 노이즈가 생기고 신호가 약해지므로 signal 증폭

Data link Layer

  • NIC(network interface card) : 일반적으로 랜 카드를 말하며, 네트워크 어댑터이다.
  • MAC(media access control)
    • NIC의 하드웨어 주소
    • 이 세상의 모든 NIC은 모두 독자적인 MAC주소를 가진다.
  • 이더넷 프로토콜(ethernet protocol) - (LAN영역을 책임진다.)
    • 물리 계층(랜 케이블)과 네트워크 계층(IPTime)간의 LAN 통신을 책임진다.
    • Destination MAC Address : 6byte, 패킷 수신 NIC
    • Source MAC Address : 6byte, 패킷 송신 NIC
    • MTU(maximum trasmission unit) : 1500byte
      • 각각 또 헤드가 붙어야 되기 때문에 application에서 붙이는 데이터는 최대 1350바이트 정도 까지 가능하다.

Internet(TCP/IP) : IP address를 다룬다.

  • host에서 요청한 ip를 IPTIME(라우터)로 보내고 정보를 다시 라우터로 회신해 왔을 때
  • 돌아온 데이터를 다시 라우터에서 host로 보낼 때 mac 주소가 필요하다.
  • 네트워크 계층(Network Layer)

네트워크 계층

  • SK : ISP(internet service provider)
    • 라우터에 공인 ip주소를 할당해준다.
  • internet은 IP address를 다루고 Network interfac는 맥 address를 다룬다.

ARP (address resolution protocol) : 주소 결정 프로토콜

  • 브로드캐스트로 어떤 IP를 사용하는 호스트의 MAC 주소를 알아낸다.
  • 브로드캐스트(모두에서 보낸다.)
    • host1과 host2는 IPTIME으로부터 사설 ip주소를 부여 받는다.
    • host1이 host2로 데이터를 보내고자 할 때 사설 ip주소 밖에 모른다.
    • 따라서 먼저 패킷을 모두에게 쏜다. (패킷에는 보내는 사람의 mac address와 사설 ip주소 그리고 받는 사람의 mac address(00 00 00 00 - 모든 사람)와 사설 ip주소가 담겨있다.
    • 만약에 받는 사람의 ip주소가 아니면 패킷을 버린다.(host3)
    • host2는 받는 사람의 사설 ip가 동일하므로 이것에 대한 response로 mac address를 회신해준다.
    • 그럼 그것을 host1이 다시 받아서 다시 데이터와 함께 전송해준다.
    • 매번 브로드캐스트 하는 과정을 매우 비효율적이므로 한번 IP주소가 mac address가 등록된 것은 ARP cache table로 담아놓는다.
    • ARP cache table reset -> ping을 활용해 같은 라우터에 연결된 모든 컴퓨터의 맥주소를 받을 수 있다.

IP 프로토콜

  • Source address : 내 ip주소
  • Destination address : 내가 data를 요청한 곳의(예 : youtube) ip주소
  • TTL(time to live)
    • 만약에 서버가 없어졌다고 하면 언제 패킷을 버릴 것이다.
    • 패킷은 지속적으로 돌아다닐 수 있다. 라우터를 하나 지날 때마다 TTL을 하나씩 줄임
    • TTL이 0이 되는 순간 도착 했는 것과 상관없이 무조건 버린다.
  • Protocol : 몇개를 TCP. 몇개를 UDP로 할 것인가?

IPV4 (IP version 4 : 4byte로 ip주소 표현)

  • 기본 틀 : 1byte - 1byte - 1byte - 1byte ( 0 ~ 255, 0 ~ 255, 0 ~ 255, 0 ~ 255)
IP class
  1. 네트워크 주소 : 라우터 주소
  2. Host 주소 : lan에 소속되어 있는 어떤 특정 호스트를 찾는 주소(컴퓨터)

class A : 네트워크는 2 ** 8 승 만큼 host는 2 ** 24 까지 부여 가능 ( 초창기)

  • 네트워크 주소가 0으로 시작 . 뒤에는 01111111 까지 가능해서 127까지

class B : 네트워크는 2 ** 16 승 만큼 host는 2 ** 16 까지 부여 가능 ( 네트워크가 늘어나고 있음)

  • 네트워크 주소가 10으로 시작
  • 10 / 00 0000 부터 10 /11 1111 까지 (128 ~ 191까지)

class C: 네트워크는 2 ** 24 승 만큼 host는 2 ** 8 까지 부여 가능 ( 라우터의 수가 굉장히 늘어남)

  • 네트워크 주소가 110으로 시작
  • 110/0 0000 부터 110/1 1111 까지 ( 192 ~ 223)까지
  • host는 255가 아니라 2명을 뺴서 253개 까지 배당가능 (ping(broadcast 255)와 나 자신(0))

서브넷 마스크

ipv4의 공인 주소가 고갈되어 감으로서 그 속도를 늦추고 host 부분을 쪼개어 낭비되는 공인주소가 없도록 하기 위한 방법

예를 들어 class C에서 host가 253명까지 가능한데 부서가 여러개로 나누어져 있다면 각 부서마다 공인 주소를 부여해줘야하는 문제가 발생할 수 있다. 이러면 공인 주소의 낭비문제가 발생한다. 따라서 host 부분을 쪼개어 최대 4개의 부서에 까지 배정할 수 있도록 함으로서 이러한 낭비를 줄이도록 한다.

class C :

  • host 가 253명까지 가능하나 규모가 어느정도 커지면 부서별로 따로 만들어줘야 한다. 따로 부서별로 공인 IP를 하면 되지만 비싸고 낭비이다. (회사내 총 인원 240명 / 각 부서별 60명 )
  • class C에 속하는 공인 IP를 하나만 쓰되 IP의 Host를 다시 쪼갠다.
  • host 8bit를 더 쪼갠다
  • ㅡ ㅡ / ㅡ ㅡ ㅡ ㅡ ㅡ ㅡ (subnet 2개 와 host 6개로 쪼갠다.)
  • subnet은 00 / 01/ 10/ 11 (4개 구성 가능하다)
  • 각 나눈 subnet마다 2 ** 6만큼 host 배정가능
  • host 입장에서는 subnet이 몇개 나누어졌는지 알아야 한다.(서버 담당자)
  • subnet mast를 통해 어디까지가 호스트 주소인자 알 수 있다.
  • ifconfig
    • en0 inet : 201.175.122.74
    • subnetmask: 0xffffffc0

서브넷 마스크 해석 방법

서브넷 계산
  • 201.175.122.74 (ip 주소)
  • 1100 1001 / 1010 1111 / 0111 1010 / 0100 1010
  • subnetmask 0xffffffc0
  • 255.255.255.192 (만약에 서브넷 마스크가 255.255.255.000 이면 무조건 안 나눈 것)
  • 1111 1111 / 1111 1111/ 1111 1111/ 1100 0000
  • 1100 1001/ 1010 1111/ 0111 1010/ 0100 0000 —> and로 계산
  • 위를 통해 나온 IP주소를 서브넷 네트워크주소이며 이것을 바탕으로 2 \ 6 명의 호스트에게 하나씩 할당한다.**
  • 201.175.122.74/26 (서브넷마스크의 1의 갯수 — 24개면 나누지 않은 것이다)

IP

지속적으로 ipv4의 공인 주소가 고갈되어 가면서 해결책으로 ipv6(6byte로 표현)가 나오게 되었다.
하지만 일단 ipv4를 완벽히 대체하기 까지 시간이 필요했고 그 해결책으로 공인과 사설 ip주소를 할당하게 되었다.

  1. Public ip (공인 ip 주소)
  2. Private up (사설 ip 주소)
사설 IP

위에 그림에서 보는 것 처럼 현재는 모두 class C이므로 192.168.0.0 부터 192.168.255.255 까지는 즉 2 ** 8 개 까지는 따로 할당하지 않고 사설 IP로 쓸 수 있도록 해놓았다.

공인 ip를 sk에서 사놓고 하나씩 배정해주는 것이다.
최종적으로 ip를 관리하는 단체가 있다. 나 공인 ip 너에게 팔건데 위와 같은 부분은 팔지 않을거야.

IP는 유일해야 한다. 근데 왜 장소를 바껴도 일치하냐?라는 질문에 답변을 하면
사설 IP를 할당받아 사용하기 때문이다.

sk에서 커피숍에 IP time을 팔고나면 NIC가 2개 들어있는데 NIC1개에 ISP를 준다. 공인 ip주소이다.
NIC 1개는 private ip address를 묶어 둔다.

따라서 라우터는 LAN인터페이스 하나(사설 ip)와 WAN 인터페이스(공인 ip) 1개 이렇게 2개가 묶여 있다.

router

카페에 사람들이 방문하면 라우터는 호스트들에게 192.168.0.3 / 192.168.0.4 / 이렇게 나누어서 준다.
이것은 private ip라고 한다(공인 ip는 전 세계에서 유일하다).
사설 ip는 iptime이 알아서 할당해준다. (위의 대역대 중에서 마음대로 할당해준다)

private network 상에 private ip가 있다.
private ip는 DHCP(Dynamic Host configuration protocol) 가 알아서 제공해준다.

커피숍에 앉아서 유튜브를 하면 나는 유튜브에 바로 접속할 수 있지만 유튜브는 불가능하다.
왜냐하면 나에게는 사설IP가 없기 때문이다.
사설 ip를 받은 애는 반드시 유튜브에서 보낸 data가 iptime을 거쳐서 와야한다.
실제로 만약에 host가 공인 IP가 있고 그 공인 ip를 알고 있으면 미국에서 바로 내 컴퓨터로 데이터를 보낼 수 있다.

우리 컴퓨터에는 개개인의 ip주소가 없고 mac주소만 있다.
ip주소는 우리가 속해 있는 라우터에 따라 매번 바뀐다.

랜 안에 속해 있는 사람끼리는 ARP를 통해서 맥주소를 알 수 있지만 유튜브에서 나를 특정해서 보낼 수 없다.
밖에 있는 사람들이 우리를 특정하기 위해서는 NAT를 통해야 한다.

내가 지금 속해있는 라우터의 주소 확인 방법

"traceroute amazon.com" - 우리가 방문하는 라우터를 추적하는 코드
이렇게 해서 제일 처음 나오는 ip주소가 내가 속해 있는 라우터의 공인 ip 주소이다.
whois 112.188.32.213 이것을 통해서 이 ip주소의 주인이 누군지 알 수 있다.

WAN mac주소와 (공인 ip에 묶여있는 맥주소)
LAN mac주소가 따로 있다. (private ip에 묶여 있는 맥주소)
이게 라우터가 2개 있다는 말이다.

내가 속해 있는 iptime이 또 거쳐서 나가는게 기본 게이트웨이 이다.

TCP/IP 주소 지정 및 서브넷 구성 기본 사항의 이해

NAT (Network Address translation : 네트워크 주소를 바꾼다)

NAT

나 사설 ip긴 한데 내 ip주소는 저거고 도착지는 google이야
나와 라우터는 LAN으로 묶여 있다. 사설 주소는 원래 못 나가는게 원칙이라 라우터가 바꿔준다.
사설 ip와 공인 ip를 맵핑하고(변환테이블) 구글에 쏜다.
이때 공인 ip는 어디든지 알 수 있으니깐 라우터의 공인 ip로 쏜다.(구글에서 접근 가능)
구글에서 다시 돌아온 데이터는 변환 테이블을 보고 라우터가 ARP를 활용하여 나의 맥주소로 바꾼후 나에게 보내준다.

— 정말 옛날 시스템이다. 라우터가 2개의 공인 ip가 있으면 맵핑을 할 때 2명밖에 불가능하다.

해결책 NAPT (Network Address Port Translation)

  • 라우터의 NIC가 딱 2개고 하나는 LAN 하나는 WAN에 물려있다.
napt

라우터가 받은 다음 맵핑을 하되 포트까지 기록을 해놓는다.
기본적으로 맵핑하는 공인 ip와 사설 ip를 동일한 포트번호로 쓰지만
192.168.0.2 : 6000번과 192.168.0.4 : 6000번이 동시에 요청하면 포트번호를 한개는 6060으로 바꾸어서 저장한다.

이것을 통해서 우리가 같이 유튜브 영상을 볼 수 있고
대신 밖에 있는 사람은 우리에게 직접적으로 무언가를 요청할 수 없다.

나는 유튜브의 공인 주소를 알고 있으므로 라우터를 거치지 않고 바로 data를 request할 수 있다.
근데 유튜브는 나의 공인 주소를 알 수 없으므로 특정해서 데이터를 요청할 수 없다.

이때 해결책이 port forwarding이다.

라우터야 내 사설 ip가 192.168.0.2:6060인데 너 혹시
217.111.222.333:1010을 나한테만 좀 걸어줄 수 있어?

1:1로 바인딩해놓으면 다른 사람들은 이 포트를 못 쓰므로 유튜브가 위로 요청하면 나에게 요청하는것과 100% 일치한다.

2019.04.02 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. 리눅스의 접근, tar, booting, user

리눅스

  • access
  • tar
  • booting
  • user

access

mkdir - directory 만들기

touch - 새로운 파일 만들기 or 최종 수정일자 바꾸기 (없다면 만들고, 있다면 최종 수정 날짜를 만든다)

권한 설정해주기 chmod (change mode)

ex)

$ ls -l
-rwxr--r-x 1 greg greg 400 3월 15 22:10 text.txt
#소유자 / groups/ ohters/ 링크의 갯수 / 소유자 / 그룹 / 바이크 크기 / 최종 수정일자

r: read w: write x: execute
사용자 / 그룹 / 나머지

그룹에 대한 권한을 숫자를 합한 값으로 한자리로 표현할 수도 있다.

0 = --- = 0+0+0.
1 = --x = 0+0+1.
2 = -w- = 0+2+0.
3 = -wx = 0+2+1.
4 = r-- = 4+0+0.
5 = r-x = 4+0+1.
6 = rw- = 4+2+0.
7 = rwx = 4+2+1.

chmod 777 text.txt  # 권한 바꾸기(7은 모든 것을 주는 것이다.)
$ ls -l
-rwxrwxrwx 1 greg greg 400 3월 15 22:10 text.txt

chmod u+r text.txt (user에게 read권한 추가)

chmod g+w text.txt (group에게 write 권한 추가)

chmod o=rwx text.txt (others에게 read, write, execute 권한 추가)

하드링크 & 심볼리 링크

ln test.txt test2.txt - 하드링크

하드링크를 걸어준다. 즉 메모리에는 같은 공간에 있지만 이것을 가르키는 것이 2개로 되는 것이다.
원본 파일이 삭제 되더라도 inode가 같으므로 여전히 사용 가능하다. (원본 파일을 삭제하더라도 레퍼런스 카운터가 0이 되지 않으므로 여전히 사용가능하다)
test와 test2가 링크로 연결되고 하지만 서로 저장 공간은 같다.(같은 하드디스크 안에서만 생성 가능)

ln -s text.txt t - 심볼릭 링크

원본 파일의 이름을 가르키는 링크이다. 원본 파일이 사라지게 되면 역할을 수행할 수 없다.
심볼리링크는 ssd와 하드디스크를 서로 연결 시켜줄 수 있다.

심볼릭링크

위를 통해 심볼릭링크는 다른 id를 가지고 있고 하드링크(test.txt, test2.txt)는 같은 id주소를 가르키는 것을 볼 수 있다.

ls - i (아이노드 - 메모리의 저장 공간 주소)

ls -F : 이게 실행파일인지 심볼리 링크인지 하드링크인지 어떤 것인지 다 볼 수 있다.

file python3 : 이것을 통해서 이 파일이 무엇인지 다 확인해 볼 수 있음

파이썬 특수권한

  1. setUID : 설정된 파일을 실행할 때 일시적으로 파일 소유자의 권한을 얻어 실행할 수 있도록 한다.
  2. setGID : SetGID가 설정된 파일을 실행할 때 일시적으로 파일 소유그룹의 권한을 얻어 실행하도록 합니다.
  3. Sticky Bit : Sticky Bit가 설정된 디렉토리에 파일을 생성하면 해당 파일은 생성한 사람의 소유가 되며 소유자와 root만이 해당 파일에 대한 삭제 및 수정에 대한 권한을 가질 수 있습니다. 즉, Sticky Bit가 설정된 디렉토리안에 누구나 파일을 생성할 수는 있지만 삭제는 본인과 관리자만 가능하게 되는겁니다.

참고 🙂

[UNIX / Linux] 특수 권한(setuid, setgid, sticky bit) :: 오늘도 난, 하하하
리눅스 특수권한(SetUID, SetGID, Stickybit) : 스마일서브 공식 블로그 [ IDC HOWTO ]

vim의 모드

  1. normal — 켜자마자 나오는 모드
  2. insert mode —> normal에서 i를 눌러야 한다. esc는 해제
  3. visual mode —> 블록 설정 normal 에서 v를 눌러야 한다.
  4. 항상 명령은 normal에서 수행 shift + : ==> 메뉴바
    1. 빠져나가기 :q
    2. 저장하기 : wq (저장하고 나가기)
    3. 좌우이동 : hjkl 만들기

apt_tar.md

tar : 묶는 것만 한다. (type archive)

cvf

  • c : create
  • v : print info of files
  • f : consider the next name as the name of archive
$ tar cvf exam.tar exam1.txt exam2.txt exam3.txt
  • exam.tar로 exam1.txt와 exam2.txt와 exam3.txt를 묶어라

cvf로 합쳐진 요소들을 보는 것 tvf

  • t : table of contents
$ tar tvf exam.tar
  • exam.tar로 묶인 것들을 확인할 수 있다.

cvf로 합쳐진 것을 푸는 것 xvf

  • x : extract
$ tar xvf exam.tar
  • exam.tar로 묶인 것들을 푼다.

booting

BIOS (basic input output)

  1. check hardware 를 해서 순서를 체크하고
  2. boot loader( GRUB )를 실행

리눅스의 서버의 용량이 모자라면 aws를 더 사면 되는데
하드디스크를 하나 더 사서 꼽는다. 그렇게 해서 사용하기 위해서
아래와 같은 행동을 해야 한다.

마운트를 태우기 위해서는

  1. 일단 파티션을 나눈다.
    1. swap - ram크기와 비슷하거나, 2배 정도 잡는다.(가상메모리)
  2. 파일시스템을 만든다.
  3. 이후에 마운트를 한다.
    1. 리눅스에 설치를 할 때 디렉토리에 연결해야 한다.
  4. 사용 이후에 un마운트를 한다.

user

서버에 대한 권한을 준다.
사용자를 추가한다. 그리고 그 사용자에게 비밀번호를 설정하게 하고
그러한 사용자들을 그룹으로 묶을 수도 있다.

리눅스 사용자 추가

sudo useradd -s /bin/sh -m -d /home/john john

사용자 비밀번호 설정

sudo passed john

리눅스파일에 대한 사용자 바꾸기

sudo chown greg test.txt #change own

리눅스 파일에 대한 그룹 바꾸기

sudo chown :greg test.txt

그룹 만들기

sudo grouped wps10

그룹 만들어졌는지 확인

cat /etc/group

그룹에 멤버 추가하기

sudo gpasswd -a greg wps10
cat /etc/group 를 해서 추가됬는지 확인

폴더에 대한 사용자 바꾸기 (가장 하위까지 다 바꿈)

sudo chown -R greg:wps10 test_dir

그룹의 비밀번호 설정

sudo gpasswd wps10

그룹에 사용자 가입

newgrp wps10
그리고 패스워드 입력

그룹지우기

sudo grouped wps10
cat /etc/group 으로 확인

사용자 지우기

sudo userdel -r john
cat /etc/p

  • 리눅스가 필요할 때 우분투 리눅스 이종원님 책 참고하자!

2019.03.29 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. 컴파일 언어 & 인터프리터 언어

  • 컴파일 언어와 인터프리터 언어는 컴파일 타임이 있느냐 없느냐, 즉 소스 코드를 분석하는 시점과 입력 데이터를 받는 시점이 언제냐에 따라 나뉜다.
  • 컴파일러 언어는 소스코드를 분석하는 컴파일 시간과 입력 데이터를 실행하는 런타임 시간이 따로 나누어져 있다.
  • 인터프리터 언어는 소스코드를 분석하는 컴파일 시간이 따로 없고 소스코드와 입력 데이터를 동시에 입력받아 결과물을 출력한다.

한장으로 먼저 보기

컴파일언어 인터프리터언어

컴파일 언어

컴파일 언어

출처: 컴퓨터 사이언스 부트캠프 with 파이썬 인터프리터 언어 분석

c언어를 예로 들면

  1. 소스 코드를 컴파일 하여 목적 파일 혹은 목적 코드인 기계어로 된 인스트럭션을 만들어 낸다.
  2. 링커는 필요한 라이브러리를 가져오고 여러개의 목적 파일을 묶어 실행 파일을 생성한다.
  3. 이제 프로그램을 실행하고 입력 데이터를 입력하면 결과 값을 도출한다.

인터프리터 언어

인터프리터언어

출처: 컴퓨터 사이언스 부트캠프 with 파이썬 인터프리터 언어 분석

파이썬을 예로 들면
일반적으로 컴파일러는 렉서와 파서로 구성된다.

렉서

렉서는 소스 코드(컴퓨터 입장에서는 문자)를 입력 받아 여러개의 토큰으로 변경시킨다.
토큰은 "나는 사과를 먹었다"는 문장을 <주어, "나는"> , <목적어, "사과를"> , <동사, "먹었다">와 같이 변수 또는 함수 이름, 연산자등으로 소스코드를 나눈다.

파서

파서는 토큰을 분석하여 분석 트리를 구성한다.
그리고 이 분석트리를 이용하여 추상 구문 트리를 만든다.
추상 구문 트리(Abstract Syntax Tree, AST)란 소스 코드의 구조를 나타내는 자료 구조이다. 추상 구문 트리를 바탕으로 심벌 테이블을 만들고 바이트 코드를 생성할 수 있다.

심벌 테이블은 변수나 함수의 이름과 그 속성을 기술 해 놓은 테이블이다.

추상 구문 트리와 심벌 테이블을 바탕으로 바이트 코드 인터스트럭션을 생성한다.

실행

그리고 이 바이트 코드 인터스트럭션은 파이썬 가상 머신인 PVM을 통해 실행된다. PVM은 굉장히 큰 무한 루프이다.

결론

  • 컴파일 언어는 대표적으로 C언어가 이에 해당하고, 대다수의 프로그래밍 언어가 이에 해당한다. 작성한 코드를 기계어로 번역을 해놓기 때문에
    실행 속도가 빠르고 보안성이 높다.
    하지만, 코드 수정을 조금이라도 한다면, 다시 컴파일을 해야 하기 때문에
    개발 기간이 오래 걸리지만, 개발 후 실행 속도는 가장 빠르다.

  • 인터프리터 언어는 대표적으로 python언어가 이에 해당되고, 컴파일 언어와는 달리 코드를 한 줄씩 번역, 실행하는 방식이다. 따라서 실행 속도는 컴파일 언어 보다 느리지만, 코드 수정시 바꾼 부분만 번역, 실행하여 빠르게 수정이 가능하다. 심지어 실행 중에도 수정이 가능하다.
    그리고, 컴파일 언어보다 문법이 쉬운 편이다. 단, 보안성이 떨어지는 편이다. 하지만, 작성이 빠르기 때문에, 빠른 아이디어 구현이 쉬워 빠른 프로그램 구현이 가능하다.

2019.03.26 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  • program 와 process 그리고 thread
  • process
    • process state
    • scheduling
      • 선점형 스케줄링(preemptive scheduling)
      • 비선점형 스케줄링
      • 구현 알고리즘
        • 우선순위 알고리즘(특징, 문제점, 해결책)
        • 라운드로빈 알고리즘(특징, 문제점, 해결책)
    • context switching
      • PCB(process control unit)
      • 문제점
  • thread
    • 정의
      • process 안의 실행흐름(인스트럭션)의 단위(“프로세스 내에서 실행되는 여러 흐름의 단위”)
      • TCB
    • multi process & multi thread
    • race condition(경쟁 조건)
      • mutual exclusion(상호배제)

program와 process 그리고 thread

  • program : HDD에 저장되어 있는 image
  • process : 실행 중인(ram에 메모리가 올라왔다) 프로그램/ 운영체제로부터 자원을 할당받는 단위
  • thread : 할당받은 자원을 이용하는 실행의 단위/ 프로세스 내에서 실행되는 여러 흐름의 단위

프로세스

프로세스 스테이트(process state)

미리 알고 가기

  • program은 1개지만 같은 프로그램을 2개 띄울 수 있음/ 단 완전히 다른 메모리를 가짐
  • 더블클릭을 하면 각자의 vas(virtual address space)가 생김
  • 각자만의 가상 공간이 생긴다.
  • 서로 다른 공간인지 구분은 PID번호로 구분 PID(process ID)
    • 윈도우에서는 랜덤으로 발급
    • 리눅스에서는 순서대로 발급

process state

process state
  • 우리가 더블클릭을 하는 순간 바로 램을 할당 받는 것 같지만 그렇지 않다.
    • RUN Queue로 들어간다.(priority queue 우선순위 큐)
    • 단 우선순위가 빠른 실행이 오면 제일 앞으로 온다.(heap와 트리로 구현할 것이다)
    • Run Queue로 들어간게 만약에 running에 들어있는 것보다 더 빠르면 running이 바뀐다.(디스패치)
    • running은 항상 프로세스 1개만 실행된다.(1개의 cpu기준)
    • running에 올라가 있던 애는 waiting으로 다시 내려온다.(프리엠션)
    • 연산작업이 아닌 I/O(입출력)작업을 할 때는 blocked로 내려간다.
      • I/O(input /output)
        1. 파일 i/o
        2. 네트워크 통신
        3. keyboard(in) monster(out)
        4. i/o가 많이 쓰인다 i/o bound라고 하고 / cpu가 많이 쓰인다. cpu bound라고 한다.
        5. async Io 는 쓰레드를 i?
    • I/O 작업이 끝나면 다시 waiting(실행가능) 상태로 돌아가서 대기한다.

scheduling

정의 : 운영체제가 여러 프로세스의 cpu할당 순서를 결정하는 것

  1. 선점형 스케줄링(pre-emptive scheduling)

    • 위의 것을 지원해서 멀티테스킹이 가능하다.
    • 선점형 os(지금 돌고 있는 애를 아무때나 정지시키고 다음에 실행될 프로세스를 올림)
      • 어떤 상황인지는 전혀 고려하지 않는다.
    • 마치 동시에 실행하고 있는 것처럼 작업을 할당(round - robin 알고리즘)
  2. 비선점형 스케줄링(non - pre-emptive scheduling)

    • 프로세스가 종료되거나 I/O작업에 들어가 명시적으로 cpu를 반환해야지 다음으로 넘어감
  3. 스케줄링을 하는데 구현 알고리즘이 존재

    • priority algorithm

      • the higher priority, the earlier running
      • 우선 순위가 계속 낮은 아이는 할당받지 못함.
          * 기아상태 발생 —> 일정시간 cpu를 할당받지 못하면 우선순위를 높임—> 에이징
    • round-robin algorithm (고의적으로 높게 잡지 않으면 다 비슷할 것이다)

      • if same priority (모든 프로세스에게 일정 시간 할당, 최대쟁점은 내가 아무리 중요한 일을 하고 있어도 일단 끌어 내림)
      • time slice(quantum 퀀덤) : 프로세스에서 할당하는 시간
      • sys.getswitchinterval() : 이것을 통해 time slice가 얼마인지 확인 가능
  4. scheduler(job scheduler는 언제 동작하나?)

    • when
      • every time slice : 타임 슬라이스 마다 실행
      • process가 실행되었을 때, process가 끝날 때
        • round-robin을 하고 있는데 먼저 끝나버릴 때
      • a running process 가 (I/O혹은 system(ad)) 블로킹이 걸릴 때
        • blocked가 걸리면 따로 빠져서 io작업을 한다.
        • io작업이 다 끝나면 os가 시그널을 쏘아주고 waiting으로 간다.
        • round/robin으로 내 차례가 올 때까지
        • 또는 system call이 되어도 blocked로 빠진다.
          • os kernel안에 시스템 프로그래머가 가져다가 쓸 수 있도록 열어놓은 함수가 있음
          • ex)sleep() —> 거이 양보해
        • waiting은 언제든지 실행될 수 있고 blocked는 i/o가 끝날 때 까지 실행못함

콘텍스트 스위칭(엄청 많이 쓰는 이야기이다.)

  • context는 현재 cpu의 레지스터에 있는 값들을 context라고 한다.
  • 지금 실행되는 애를 내리고 지금 실행해야 될 애의 context를 스케듈러가 다시 cpu로 내릴 때를 컨텍스트 스위칭이라고 한다.
컨텍스트 스위칭
  • 현재 실행되고 있던 A가 cpu-bound 관련된(for문)걸 하고 있을 때 cpu의 자원을 뺏김
  • 계산이 끝나고 마지막 올리기만 하면 되는 경우 쫒겨날 때 memory 상에 pcb(process control block)를 만들어서 cpu의 담겨져 있는 것을 저장한다.
  • PCB(Process control unit)을 통해 컨텍스트 스위칭이 이루어진다.
    • PCB에는 프로그램 카운터, 스택 프레임의 정보(스택포인터, 프레임 포인터), 연산 중인 데이터가 저장된 범용레지스터 등이 포함된다.

콘텍스트 스위칭의 단점

  • 자주 일어날수록 사용자는 멀티 테스킹이 동시에 일어난다고 보인다.
  • 하지만 타임슬라이스를 너무 짧게 잡으면 20cycle 왕복 40cycle ~ 200cycle씩 계속 일어나면 컴퓨테 os에게 큰 부담을 주는 것이다. 짧게 잡으면 완전 멀티테스팅처럼 보일 것이고, 길게 잡으면 os에게 부담을 덜 줄 것이다. 이것을 얼마만큼 설정할지가 굉장히 중요하다.

thread

  • 정의

    • process 안의 실행흐름(인스트럭션)의 단위(“프로세스 내에서 실행되는 여러 흐름의 단위”)
    • TCB
  • thread의 필요성

    • 인스트럭션의 나열 —> 함수 실행
    • concurrency programming (동시성 프로그래밍)
      • 언제 필요하냐?
        • 어떤 서버에서 200g짜리 데이터를 받고, 그 데이터를 데이터 전 처리(orocessing)을 하고 분석하고 싶을 때 먼저 데이터를 다 받아야 할 것이다.
        • 이렇게 하는 것이 아니라 multi thread를 활용해 한곳에는 데이터를 받고 한 곳에서는 데이터 분석한다.

어떻게 구현하나?(multi process & multi thread)

  • multi-process(여러개의 프로그램을 동시에 실행하여 multi로 처리한 것 처럼 보이게 한다.)
    • 치명적 단점 : shared data가 필수적으로 발생하나, 서로 다 따로 작동되므로 각 데이터를 다 따로 가지고 있어야 한다.
    • IPC (inter process co…)를 통해 데이터를 공유하여 해결할 수 있으나 힘들다.
멀티process
  • multi - thread
    • thread는 프로세스 안에 포함된다. 쓰레드는 stack만 나누어 가질 뿐이지 code ,data, heap은 완벽히 공유한다.
    • 이렇게 되면 각각 실행하는 것을 쓰레드가 각각 맡아서 하고 나머진 다 공유한다.
    • 왜 스텍은 공유할 수 없는가?
      • 뜨레스는 인스트럭션의 나열이다.
      • thread1 은 함수 1,2
      • thread2 은 함수 3,4
      • 뜨레드가 스텍만 따로 가지고 있으면 함수를 나누어서 실행가능
  • i/o bound한 프로그램이라면 (네트워크 관련) asynchronous I/O 비동기 i/o

<multi - tread>
멀티thread

thread 구현

  1. GIL (Global Interpreter Lock)
    1. 파이썬의 큰 단점
      1. multi 코어라도 바이트 코어의 실행을 한번에 하나의 쓰레드로 막는다.
      2. 코어가 2개라도 바이트 코어의 실행을 한 바이트로만 실행하게 한다.
      3. GIL을 아직 해결하지 못했다.
      4. 멀티쓰레드 스레딩을 해도 속도가 단축이 안된다.
      5. gil이 있기 때문에 multithreading을 해도 시간상의 성능향상은 어렵다.
  2. 잘못된 생각
    1. 4sec가 걸리는 것을 1초 단위로 나누어서 실행하면 더 빠를 것 같지만 결국 4초와 맞먹는 시간이 걸린다.
    2. 스피드를 위해 concurrency를 하는 것은 아니다.
      1. for multitasking
  3. single core, multi thread 일 때
    1. concurrency를 해결해주어도 스피드를 해결해 주진 않는다.
  4. 듀얼 코어 , 4개 thread 일때
    1. concurrency를 해결해주고 스피드도 해결
  5. 4개 코어, 4개 thread일 때
    1. concurrency이자 parellerism programming 처럼 된다.

multi-tread의 문제 (race condition 발생)

예).

  1. 글로벌은 메모리 데이터에 저장된다.
  2. sum += 1
    1. move eax, [g_num]
    2. add eax , 1
    3. move [g_num], eax
  3. round-robin에 의해 덜 되었어도 스케줄러가 바꾸어 버림
  4. 따라서 실행이 덜 되었는데(2번째 인스트럭션까지 실행되었는데) 넘겨지는 상황이 발생함
  5. 레이스 컨디션(서로 공유자원을 쓰기 위해 경쟁한다)

race condition 해결책

  1. mutual exclusion : 상호배제
  2. 화장실 생각. 서로 들어갈려고 한다. 즉 누가 들어갔을 때 락을 걸어버린다.
  3. 한번 들어갔던 애가 비워줄 때 까지 락을 걸었다가 풀었다가 한다.
  4. 하지만 상호배제를 하면 쓰레드를 쓰나 마나가 되고 오히려 더 안 좋아질수도 있다.

참고사항

  1. 코어 안에 담겨져 있는 thread와는 우리가 지금 배우는 thread와 다르다.
  2. 하지만 alu를 쓰지 않고 작업하는 것은 나누어 볼 수 있지 않을까?가 thread이다.
  3. 이것을 이해하기 위해서는 파이프 라이닝을 또 공부해야 한다.

2019.03.16 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기.

  1. 우리가 입력한 정보는 어떻게 메모리에 저장되는가?
  2. 변수를 스택에 할당하면 왜 빠르고 힙에 할당하는 왜 느린가?
  3. 언제 힙을 사용하는가?
  4. 함수를 호출할 때 어떻게 스택프레임이 쌓이는가?
  5. 가상 메모리란 무엇인가? 페이지와 스왑은 무엇인가? 디스크와 메모리의 속도차이는 어느정도이고 그 원인은 무엇인가? SSD의 장점은 무엇인가?
  6. 10기가바이트짜리 파일을 C 드라이브에서 D 드라이브로 복사를 한다. 얼마나 걸릴까? A 폰에서 C 드라이브로 옮겨온다면? 만약 1메가바이트짜리 1만개라면 얼마나 걸릴까? 카피를 동시에 여러개 돌리면 더 빨라질까?

한번에 보기

메모리

메모리(memory)

# 미리알면 좋은 정보.

  • 메모리 저장 방식에는 빅 엔디언과 리틀 엔디언이 있다.
    • 사람은 빅 엔디언 방식으로 사용하고
    • 컴퓨터는 대부분 리틀 엔디언 방식으로 저장한다.
    • 단 네트워크를 이용할 떄는 빅 엔디언 활용.
    • 참고

메모리 계층(hierarchy)

  1. 레지스터
  2. 캐시(cache 미스와 cache hit)
  3. 메모리
  4. hard disk(기계식) / SSD (전자식)
  • 위로 올라갈수록 빠르다.
  • 위로 올라갈수록 용량이 작다.
  • 레지스터가 그렇게 좋으면 다 레지스터로 구성하면 안되냐? ---> 비싸다

메모리 특징

  • 하드디스크에 있는 데이터는 바로 레지스터로 갈 수 없다. 모든 데이터 이동은 순차적으로 이루어져야 한다.
    • register ---> ALU or 다른 register ----> 1cycle
    • Cache ---> register ---> 3 cycle
    • memory ----> register ----> 20cycle ~ 100 cycle
    • hard disk ---> register ----> 50만 ~ 5000만 cycle

위와 같은 속도 차이로 인해 프로그래밍 할 때 데이터 베이스에 최소한 적게 접근해야 한다.
SQL은 데이터베이스로 접근하기 위해 쓰는데 최소한 적게 써야 한다.

Cache도 결국 속도를 빨리 하기 위한 것이다.

cpu가 3ghz짜리가 나와도 memory에서 데이터를 가지고 오는데 시간이 오래 걸리면 다 소용이 없다.
그래서 나온게. 캐시이다.

ex)

>>>li = [1,2,3,4,5]
>>>res = 0
>>>for e in li:
        res += e

---> 위의 코드는 바로 계산 못한다 cpu로 가지고 와야 한다.
먼저 위의 값들이 어떻게 메모리로 저장되는지 알아야 하고 그리고 그 저장된 것을
어떻게 cpu로 가지고 오는지를 알아야 한다.

  • 기본적으로 300cycle이 실행된다.
    1. res에 계속 접근한다 . --> temporal locality
    2. li에 순차적으로 접근한다. ---> spatial localit

위의 코드를 실행할 때 cpu는 메모리에 위와 같이 접근한다고 볼 수 있다. 따라서 여기서 지역성이라는 것이 나오게 된다.

  • locality는 2가지 개념이 있다.
    1. 시간지역성temporal locality --> 한번 접근한 메모리에 자주 접근
    2. 공간지역성spatial locality ---> 접근하는 메모리가 이전에 접근한 메모리의 근처일 확률이 높다.

위에 2가지 특징을 알게 되었고 그래서 이것을 해결하기 위해 캐쉬라는 것을 설치했다.

먼저 CPU ---> CASHE에게 list값이 있는지 물어보고 없으면 메모리에서 cashe에 저장한다.
list만 가져오는 것이 아니라 list 주변의 메모리 데이터를 듬뿍 가지고 온다.(대략 64byte ~ 128byte cachelines)

  1. CPU가 캐시에 데이터를 요청했는데 데이터가 없어서 메모리에 요청할 때 ----> 캐시 미스
  2. CPU가 캐시에 데이터를 요청했을 때 캐시에 데이터가 있을 때 ----> 캐시 히트

기본적으로 캐시히트가 일어날 확률이 95%이다.

캐시의 위치
cpu --- ram 사이에 존재하고 있었음
but 지금은 L1캐시는 cpu안에 있고 L2, L3는 밖에 있음

가상 주소 공간

  • 하드디스크의 페이지파일을 메인 메모리 처럼 사용 => 가상 주소 공간
  • 가상 주소 공간(假想 住所 空間, 영어: Virtual Address Space; VAS)은 가상 메모리 기법으로 제공되는 주소 공간으로서, 프로세스의 관점에서 사용하는 주소이다.
  • 프로그램을 더블클릭하여 실행하면 하드디스크에 있던 프로그램이 메인 메모리로 올라오면서 프로세스가 생성되고, 32bit운영체제라면 실행되는 순간 4GB 메모리를 할당받는다.

할당 받은 램 4g (32bit 일 때)

크게 user영역과 kernel영역으로 나누어진다.

2g (user 영역)

  • 코드
  • 데이터
  • 스텍

2g ( kernel 영역 - os를 실행시킬 수 있는 데이터들이 들어간다.)

  • 코드
  • 데이터
  • 스텍

옛날은 pc가 직접 메모리 주소를 가르켰다.
--> 잘못하면 커털영역의 메모리 주소를 불러오게 할 수 있고 그러면 블루스크린이 뜨면서 시스템이 꺼진다.

지금은 os가 커널 영역을 직접 관리해서 보안이 훨씬 나아졌다.

하지만 실제로 내가 프로그램을 짤 때는 나에게 운영체제가 무조건 메모리를 4g를 준다고 생각하고 설계하면 된다. 왜 그럴까?

바로 가상 주소 공간(vas) 때문이다.

  • 가상 주소 공간은 가상 메모리 기법으로 제공되는 주소 공간이므로 가상 메모리에 대해 알아야 한다.
  • virtual address space(메모리가 아니다.) 우리는 메모리 처럼 쓰면 되지만 실제로는 메모리가 아니고 os가 우리에게 주는 가짜 메모리 공간이다.

가상 주소 공간에 대해 좀 더 명확히 알기 위해서는 메모리에 대해 좀 더 알아야 한다.

우리가 코드를 작성하고 저장하면

  • 우리가 작성한 함수나 클래스 정의 코드는 인스트럭션으로 컴파일 되어서 하드에 저장되었다가 호출할 때 코드 세그먼트로 들어가는 게 된다.
  • 데이터 세그먼트에는 전역변수가 들어간다. BSS 포함이다.(이건 뭐지?)
  • 스텍에는 지역변수가 쌓이는 공간이다. 스텍프레임이 쌓이는 공간이다.

변수를 왜 저렇게 나누어서 저장할까?

바로 생성 시기의 문제이다.

  • 예를 들어 전역변수는 프로세스가 시작 될 때 생성된다.
  • 소멸시기는 프로세스가 종료 될 때 이다.
  • 생성과 소멸에 대해 프로그래머가 할 수 있는 일이 없다.

아주 잠깐만 필요한 데이터 인데 전역 변수에 넣어 놓으면 굉장히 메모리 낭비이다.
따라서 전역변수를 되도록 쓰지 마라고 한다.!!!

이에 비해 스택프레임이 생성될 때는 함수가 호출 될 때이다.
소멸은 함수 실행이 종료될 때 이다.
즉 프로그래머가 어느정도의 통제권을 가지고 있다.

if 함수 호출이 종료되고 나서도 데이터를 가지고 있고 싶으면, call by reference를 사용한다.

>>>int a = 10; 
>>>func(num):
>>>    num += 100 이면 
>>>func(a)
>>>int a 

라고 하면 a의 값은 바뀌지 않는다. ---> call by value

함수 호출이 끝난 다음에도 유지하기 위해서는 call by reference를 해주면 된다.
따라서 b = func(a)으로 해서 전역변수 b를 해주면 남길 수 있으나 이것은 전역변수를 또 생겨나게 하는 것이다.

그래서 나온 개념이 heap이다.

heap은 생성시기는 프로그래머가 원할 때 소멸시기도 프로그래머가 원할 때 이다.
함수 내부에서도 만들 수 있고 없앨 수도 있다. 엄청난 특권이다.
하지만 맹점이 있다. 생성만 해주고 소멸을 안하면 그대로 남아 있는다. (메모리 누수).
또한 할당과 해제가 잦아 메모리 빈 공간이 잘게 나누어 지면서 총합은 충분하지만 관련 데이터가 한 곳에
모이지 못하기도 한다. 이를 메모리 단편화라고하고 메모리 단편화가 발생하면 캐시 미스 확률이 높아진다.

heap의 단점

  1. heap에 할당은 했는데 지을 수 없을 때 ---> 메모리 누수(memory leak)
  2. malloc이 느리다.
  3. 메모리 단편화(memory fragmentation)

stack 과 heap 비교

stack은 메모리를 할당할 때 차곡 차곡 쌓음
해제를 할 때도 위에서 부터 해제해서 메모리 레이아웃이 연속적이 된다.
---> 할당 할 주소를 찾을 필요가 없음(stack pointer) ---> 스피드가 빠르다.

heap에 쌓을 때 (double linked list)

  1. malloc()
    처음 쌓을 때 왼쪽위부터 그 다음에 이어서 오른쪽으로 쌓음
    그리고 아래에도 이어서 막 쌓음. malloc을 가르키는 주체는 stack프레임에 있음(변수).
    지우게 될 때 중간 중간에 막 지우게 된다.
    malloc이 쌓일 때 가장 앞쪽에는 header를 넣음(주소값 및 크기) 이 헤더가 linked list로 구현되어 있음
    이 header들은 지워지지 않고 남아져있음. 따라서 os 가 이 header들을 찾아다니며 넣을 수 있는 곳을 찾음

  2. if 딱히 저장 할 곳이 없을 때(1개의 크기 문제 or 공간이 꽉 차서)
    남는 곳으로 보면 충분히 넣을 수 있는데 메모리가 조각조각 비워있으니깐 (단편화가 되어있어서) 메모리를 할당할 수 없음.

---> 메모리 단편화(전체로 봤을 땐 충분하지만 메모리가 쪼개져 있어서 메모리를 할당 할 수 없거나 찾는데 오래 걸리는 경우)
stack에는 메모리 단편화가 생길 수 없음

메모리 단편화 해결방안

  • 제일 먼저 잡히는 힙의 공간을 default heap이라고 한다.시작을 하자마자 한방에 malloc에 1.6g정도 요청을 함. (heap은 메모리 바운더리가 없다. stack은 한계가 정해져있다)

일반적으로 user에게 2g가 할당되면

  • code - 17m
  • data - 4m
  • heap
  • stack - 1m

약 1.98g 만큼이 남는데 이만큼 heap이 커질 수 있다.
시작하자마자 malloc으로 1.98g을 받아놓고
이거를 프로그래밍을 통해 내 마음대로 쪼갤 수 있고 나만의 메모리 매니지먼트를 만들 수 있다.

예를 들어 abc.py을 실행한다고 보면 먼저 python.exe 가 실행되고 abc.py이 실행되는데
code 세그먼트에 python.exe가 실행되고 abc.py는 말그대로 editor로 문자열을 python.exe에 넘겨주는 것이다.
따라서 abc.py 그 자체는 공간을 할당 받을 수 없다.
abc를 실행하던중 나오는 것들은 모두 heap에 저장 될 수 밖에 없다.
python.exe 실행하면 저 위에 메모리 전체를 갖고 (이미 실행되어 stack에는 쌓지 못하고)
heap에 쌓을 수 밖에 없다.

  • 파이썬을 실행하면 미리 malloc을 통해 일정 용량을 가지고 오고 heap을 적절하게 나누어서 활용한다.(abc 관련된 것은 heap에 쌓인다).
  • 이것을 dynamic heap이라고 한다.*

dynamic heap을 통해 메모리 단편화를 해결할 수 있다.

파이썬에서는 파이썬이 알아서 해준다.
따라서 그냥 melloc은 os가 관리하게 때문에 파이썬에서 쓰면 터질 수 있다.
PyMem.Malloc을 써라. os가 아니라 파이썬이 주도권을 가지고 와서 할당해준다.

앞으로 heap의 관리를 python이 한다 ---> dyanmic heap이라고 한다.

개발자들의 이야기 30%는 heap와 stack이야기 이다.

gabage collection :

  • 언어 차원에서 메모리 관리를 해주겠다.---> heap을 내(언어 자체)가 관리해줄게.
  • 파이썬 구현방법 ---> reference counter
  • heap에 생긴 메모리의 reference counter을 확인하여 counter가 0이 되면 파이썬이 알아서 삭제 한다.
  • python은 reference counter 기능을 통해 삭제해준다.

파이썬은 heap 안에 부분을 메모리와 완전히 똑같이 만들어 놓았다.

정리하자면

  • code data heap stack은 모두 데이터를 저장하는 공간인데
    code 세그먼트는 우리가 작성한 코드가 기계어로 바뀌어 하드디스크에 저장되었다가
    우리가 호출하는 순간 코드 세그먼트에 저장되고,
  • data 세그먼트는 전역변수가 저장되는 공간으로 프로세스가 시작될 때 생성되었다가
    프로세스가 종료될 때 소멸되어 프로그래머가 할 수 있는 일이 없으며,
  • Stack 세그먼트는 지역변수가 저장되는 공간으로 함수가 호출 할 때 생성되고,
    함수가 종료될 때 소멸되어 프로그래머가 어느정도 통제권을 가지게 되지만 함수가 끝날 때 까지는,
    계속 유지해야 된다는 단점이 있다. 이를 보완하기 위해 나온 것이
  • heap 세그먼트로 heap 세그먼트는 프로그래머가 원할 때 생성하고 원할 대 소멸시킬 수 있다.
    하지만 생성만 해주고 소멸을 안해주면 그대로 남아 지속적으로 메모리를 차지한다.(메모리 누수)

스텍 세그먼트

이번에는 스택 세그먼트에 저장되는 함수의 스택 프레임이 어떻게 할당되고 해제되는지 알아보자.

먼저 2개의 용어에 대해 알아야 하는데

  1. esp(stack pointer)
    • 스택 세그먼트의 맨위를 가르키는 스택 포인터 레지스터
  2. ebp(base pointer or frame pointer)
    • 스택 프레임의 기준
    • 지역 변수에 접근 할 때 프레임 포인터를 이용하여 이동

에 대해 알아야 한다.

스크린샷 2019-03-18 오후 12 41 28

# 사진은 컴퓨터사이언스 부트캠프 with 파이썬에서 가지고 왔습니다
출처

나는 위에 사진으로 최대한 간단하게 이해를 했는데 ebp는 스택 프레임의 중간을 가르키고 있고 ebp를 활용하여 지역 변수에 접근 할 수 있고, 스택 프레임이 새롭게 생성 될 때는 esp가 이동하여 미리 지역 변수 공간을 확보하여 새로운 스택 프레임을 생성한다. 그리고 스텍 프레임이 소멸 될 때는 다시 esp의 위치를 ebp로 옮김으로서 해당 스텍 프레임을 소멸 시킨다.

더 많은 정보를 알면 좋겠지만 ㅠㅠ.. 이 정도만 하고 넘어가려고 한다.

가상 주소 공간(virtual address space)

길게 돌아서 다시 왔다 ㅠㅠ.

  1. 가상 메모리 정의
    • 메인 메모리를 확장하기 위해 페이지 파일(page file)이라고 불리는 하드디스크의 일정 부분을 메인 메모리 처럼 사용하는 것을 말한다. ---> 리눅스에서는 swap이라고 부른다.
  2. 가상 주소 공간 정의
    • 가상 메모리 기법으로 메모리에 주어지는 가상 메모리 주소 공간

MMU

가상 메모리에서는 가상 메모리 주소가 주어지는데 이를 논리 주소(logical address)라고 하고 메인 메모리의 메모리 주소는 물리 주소(physical address)라고 한다. 실제로 프로세스를 실행하려면 메인 메모리의 물리 주소가 필요하다.
이를 위해 논리 주소를 물리 주소로 변환시켜주는 하드웨어가 MMU(Memory Management Unit)이다.

목표 : page와 pagenumber, offset의 개념

if 2000byte가 있다고 하면 주소 값은 0 부터 1999까지 있을 수 있다.
if page를 100byte로 잡으로 총 페이지 갯수는 20개이다.
처음 페이지 #0(넘버) ~ #19

0 번째 페이지 a0 ~ a 99.
1 번째 페이지 a100 ~ a 199 -----> 뒤에서 2번째 앞으로 와서 자르면 페이지의 넘버
if a145 ----> 1번째 페이지 + offset(특정한 값에서 떨어진 정도) -- 45.
145 - page # + offset
이것을 그대로 2진수에 적용

  • page : 프로세스를 실행하며 부여 받은 메모리를 쪼개어 쪼개진 한 부분
  • pagenumber : 나누어진 페이지 순서를 나타내는 비트
  • offset : 페이지 안에서 특정 바이트를 가르키는 비트

페이지 넘버와 오프셋을 더하면 논리 주소가 된다.

페이징

우리는 프로세스를 실행하며 부여 받은 4g를 어떻게 할 것이냐? 쪼갤 것이다.
in 32bit 컴퓨터 --- > 4096byte ---> 4kb로 쪼갠다

쪼개어진 4096byte를 page라고 한다. 2 ** 12

4g는 2 ** 32승 / 2 ** 12 ---> 총 페이지수는 2 ** 20

page #0 ~ page # 2 ** 20 - 1 까지 존재

따라서 32비트 시스템에서 메모리 주소를 표현할 때는 32비트를 활용한다.
페이지 갯수가 2 ** 20개 이므로 페이지 넘버를 나타내는 비트는 20 비트고,
페이지 크기가 4,096바이트(2 ** 12)이므로 오프셋은 12비트이다.
이 둘을 합치면 가상 주소 공간에서 하나의 주소 값이 된다.

실제 RAM

메인 메모리 역시 가상 주소 공간과 같은 크기로 쪼갠다. 4096byte크기로
1개씩 다 나누었을 때 이것을 page frame이라고 한다.(진짜 메모리)]]

frame의 갯수는 2 ** 20.
0 ~ frame의 마지막은 2 **. 20 - 1.

특정 frame의 주소를 보았더니 0x5252a/ef3 (16진법 표기 2진수로 바꾸면 20자리/12자리)
5252a가 frame 넘버를 의미하고
이 프레임의 첫번째 위치부터 떨어진 정도 ef3

VAS + Ram의 변환의 큰 그림

cpu가 pc를 보고 vas에게 요청 (0x12345abc)

---> 이것을 실제 램에서 가지고 와야 한다.

여기서 vas가 page 넘버와 오프셋을 쪼갬 12345/abc ---> 8자리가 16진수를 나타내는 것이고 2** 20을 나타내려면 16진수 5자리 수로 표현

만약에 실제 ram에서는 0Xabcde/abc에 매칭되어 있다면

페이지 넘버만 12345 ---> abcde로 바꿔주면 된다. ( logical address ---> physical address)

page table (page fault와 연관)

페이지 테이블은 어떤 프로세스의 페이지 넘버, 상응하는 프레임 넘버, 상태 등을 저장하는 테이블이다.
모든 프로세스는 각자만의 페이지 테이블이 있고 이 페이지 테이블은 메인 메모리에 저장된다.

만약에 우리가 어떤 프로그램을 더블클릭을 하면 os가 프로세스를 시작하면서 4g를 주는 척 하면서 램에 있는 일부 공간에 페이지 테이블을 만든다.

스크린샷 2019-03-19 오후 12 06 28

os가 프로세스를 시작하면 4G를 할당해준다.
---> 프로세스마다 페이지 테이블을 main memory에 만들고 pagetable의 첫 주소를 가르쳐준다.(4g를 할당해준다 라고 표현)

VAS를 구현하기 위한 구현체가 pagetable이다.
mapping table을 만드는 것 자체를 4g 할당했다고 한다.

if 스타크래프트를 딱 키면 4g를 준다
pagetable은 일단 3가지만 기억(칼럼)

  1. 페이지 넘버
  2. 프레임 넘버
  3. valid bit(유효 비트)

딱 키면 pagetable을 만들고
지금 당장 필요할 것 같은 코드영역의 데이터 혹은 전역변수를 하드디스크에서 4096byte 단위로 메모리에 업로드 ----> freeparing프리페어링기법

가상의 4g에서 cpu가 요청하면
페이지 테이블에서
페이지 넘버에 vas의 페이지 넘버를
프레임 넘버에 실제 램 메모리의 프레임 넘버를 저장한다.

이것의 2가지를 바탕으로 페이지 부분을 바꾸어서 pc를 바꾸어서 MAR(memory address register)에 저장해주고

MMU ---> memory management unit ---> logical address 를 physical address로 바꾸어 준다.

따라서 4g를 준다는 것은 페이지 테이블을 할당해주는 것이다.

페이지 테이블의 valid bit가 0이면 (즉 프레임 넘버가 있지 않으면, 메인 메모리안에 실제 메모리가 없다. ) ---> page fault 페이지 폴트
----> 하드 디스크에서 비어있는 공간에 램에 올리고 이거를 다시 frame 테이블에 올리고 valid bit를 1로 바꾼다.

page fault가 나면 치명적이다. 하지만 한번 가지고 올 때는 한번씩 꼭 발생한다.

캐시미스와 페이지 폴트는 비교할 정도가 아니다.

다음 코드를 실행할 떄 MMU가 pagetable을 참고하여 확인해줌

내가 스타크래프트를 하다가 져서 음악을 들으려고 유튜브를 키면

process 2가 등장한다.
(#페이지 테이블은 공유하지 않는다. 프로세스마다 한개씩 생긴다)

하드디스크 안에서 page file(리눅스 swap)이 있다.
1. 우리가 하드디스크를 메인 메모리로 사용하지 않고 램을 사용하는 이유는 속도 때문이다.
1. 만약에 하드디스크의 속도가 메모리 만큼 빠르면 메모리와 동일하다.
2. 하드디스크의 일정 용량을 마치 램처럼 사용하겠다 ---> 이 크기를 페이지파일이라고 한다. (가상 메모리)

그럼 어떤식으로 동작하나??

유튜브도 새로운 페이지테이블이 생긴다.

스카이프를 띄움

프로세스를 한 100개 띄어서 램을 꽉 채움

만약에 스타를 다시 시작하게 되면 valid bit가 0이면 페이지폴트가 나고 하드디스크의 코드를 램에 보내야 하는데 램이 가득 찻기 때믄에 새로운 애를 올리기 위해서 누군가는 내려와야 한다. ----> 내리는 애들 pagefile로 내림 이 내려온 에를 victim page라고 부른다. 이렇게 내려가는 행위를 page out이라고 말한다.(올라가는건 page in)

만약에 cpu가 다시 프로세스 2로 와서 아까 내렸던 애를 요청하면, 램에서 다시 가장 오랫동안 사용하지 않았던 애를 또 내림 (victim page, page out) 그리고 다시 내렸던 애를 page in하여서 새로운 주소를 frame table 올려줌

따라서 frame table을 항상 일정하지 않다.

가상메모리는 ram + page file을 가상메모리라고 하고 이거를 paging 이라고 한다.

if page in , page out 보다 page fault가 더 빨리 일어나면 thrashing이라고 하여서 실행은 안되면서 미친듯이 내부적으로 작동되고 있는 상황을 thrashing이라고 한다.

페이스북 이야기

왜 데이터를 힙에 저장했을 때 페이지 폴트가 많이 일어날까??

gcc가 데이터를 저장할 때 힙에 저장함. malloc의 특성에 의해서 힙의 여러 위치에 저장됨

가상 메모리에서는 특정 단위로 나누어서 가지고 오고 불러올 때 페이지폴트가 일어날 확률이 많아진다???? ----> 이해 안됨

stack에서는 한번만 페이지폴트가 일어나면 다시 일어날 확률이 굉장히 작아짐.

CPPcon 2016 : nicholas ormrod
/fbstiring cppcone/

2019.03.15 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기.

  1. C = a+b 라고 할때 cpu와 메모리 상에서는 무슨 할일이 많길래 3줄이나 필요한가?
  2. 메모리에서 어떻게 cpu로 가지고와서 실행되는가?
  3. CPU의 구조에 대해 설명해봐라

CPU

1.CPU의 구조

1

# 사진은 컴퓨터사이언스 부트캠프 with 파이썬에서 가지고 왔습니다
출처

  1. CU(Control unit)
    • 기계어를 어떤 명령인지 해석하고 이를 실행하려고 할 때 CPU의 각 파트에 지시를 내리는 역할
  2. ALU(Arithmetic Logic Unit)
    • 덧셈, 뺼셈등의 산술연산과 AND, OR와 같은 논리 연산 진행
  3. AX,BX,IR,PC(레지스터)
    • CPU안의 내장된 메모리
    • AX(Accumulator)
      • 값을 축적해가는 레지스터

CPU의 원리를 알기 위해 조합 논리 회로와 순차 논리 회로를 이해하면 CPU의 원리를 정확하게 이해 할 수 있다.

조합 논리 회로

  • 현재 입력에 의해서만 출력이 결정되는 논리 회로
  • 가산기(ALU안에 있음)
스크린샷 2019-03-18 오전 9 00 41 \# 사진은 컴퓨터사이언스 부트캠프 with 파이썬에서 가지고 왔습니다 [출처](https://thebook.io/006950/ch08/02/01/)
  • 덧셈이나 뺄셈등을 포함한 ALU(산술 논리 장치) 연산은 현재 출력이 오로지 현재 입력에 의해서만 결정된다. 이는 조합 논리 회로의 특징이다.

순차 논리 회로

# 미리 알면 좋은 플립플롭.
플립플롭 또는 래치(영어: flip-flop 또는 latch)는 전자공학에서 1 비트의 정보를 보관, 유지할 수 있는 회로이며 순차 회로의 기본요소이다. 조합논리회로에 비해 플립플롭은 이전상태를 계속 유지하여 저장한다.

  • 현재의 출력이 현재의 입력과 과거의 출력에 따라 정해지는 논리 회로
  • AND, OR , NAND, NOR과 같이 과거의 출력이 현재의 입력과 합쳐져서 현재의 출력이 결정되는 논리 회로
  • 입력이 바뀌었다고 해서 출력이 바로 바뀌는 것이 아니라 특정 상황이 되었을때만 바뀐다는 점, 이를 통해 이전 출력 값이 유지 된다는 것이다.
  • 유지 된다는 것은 곧 저장된다고 할 수 있다.
  • 이를 통해 순차 논리 회로를 구성하는 플립플롭을 메모리 소자, 즉 메모리를 구성하는 기본 단위라고 부르며, 실제로 레지스터는 플립플롭의 묶음이다.

조합 논리 회로와 순차 논리 회로를 통해 알 수 있는 것은 ALU에서 조합 논리와 순차 논리가 계산이 되며, 조합 논리 회로를 나타내는 가산기는 ALU내부에 존재하며 AX, BX와 같은 레지스터는 순차 논리 회로를 계산하여 ALU내부로 보내 가산기로 계산한다?? ---> 이거는 선생님께 추가 질문해봐야겠다.

2. 클록

CPU의 속도를 나타내기 위해 2.6GHZ와 같은 단위를 이용한다.
HZ는 주파수(진동수)를 나타내는 단위로 1초 동안 왕복하는 횟수를 나타낸다.
2.6GHZ는 1초 동안 2.6 x 1024(g) x 1024(m) x 1024(k) x hz 만큼 왕복했는 것이며 이를 주기로 나타내면 1번 왕복하는데 걸리는 시간을 알 수 있다.
이를 클록이라고 한다. 즉 1초에 몇번의 신호를 생성하는가 이다.
보통 요즘 컴퓨터의 cpu가 2.0GHZ의 속도는 나타내고 있으므로 1회당 약 20억번의 클록 펄스를 생성한다.
그리고 그 클록 펄스(신호)가 생성될 떄 마다 인스트럭션을 실행한다. 인스트럭션이란 우리가 작성한 코드가 cpu에서 실행되기 위해 최종적으로 변환된 기계어이다.
또한 이 클록과 인스트럭션을 활용하여 성능을 나타내는 지수로 CPI(Clock per Instruction)이라는 것을 쓰기도 한다. 이는 인스트럭션당 클럭의 수를 나타낸다. 즉 한 인스트럭션이 실행될 때 얼마나의 클럭이 필요하냐를 나타낸다.

3. 시스템 버스

스크린샷 2019-03-18 오전 10 19 23 \# 사진은 컴퓨터사이언스 부트캠프 with 파이썬에서 가지고 왔습니다 [출처](https://thebook.io/006950/ch08/02/01/)

시스템 버스를 설명하기 위해서는 먼저 각 레지스터의 이미부터 파악해야 한다.

  1. IR(instruction Register)

    • 변환된 명령어는 프로그램을 실행하면 메인 메모리에 올려지고 하나씩 실행되는데 이때 메모리에 있는 명령으로 CPU로 가져와 저장해 두는 곳
  2. PC(Program counter)

    • 현재 실행 중인 명령어 다음에 실행될 명령어의 메모리 주소를 담고 있는 곳
  3. AX, BX(범용레지스터)

    • 주로 메모리에서 읽어 들인 데이터를 저장했다가 ALU가 연산할 때 피 연산자로 전달하거나 연산 결과 값을 저장할 때 쓰임
스크린샷 2019-03-18 오전 10 31 33

# 사진은 컴퓨터사이언스 부트캠프 with 파이썬에서 가지고 왔습니다
출처

시스템 버스


  1. 데이터 버스(Data Bus)

    • 제어 버스의 신호에 따라 데이터를 CPU에서 메모리 전송하거나 반대로 메모리에서 CPU로 전송
  2. 제어버스(Control Bus)

    • 데이터를 레지스터로 읽어볼지(READ) 아니면 메모리에 쓸지(Write) CPU가 메모리에 전달
  3. 주소버스 (Address Bus)

    • 메모리에서 레지스터로 혹은 레지스터에서 메모리로 데이터를 전송할 때 필요한 메모리 주소를 전달

결국 메모리에 저장된 정보를 어떻게 CPU로 가지고 와서 실행할 것인가에 대해 알아가는 것이다!!. 그럼 추가적으로 우리가 컴퓨터에 입력하는 정보들이 어떻게 메모리에 저장되며 그 저장된 정보를 CPU로 가지고 오는 것은 어떻게 이루어 지는 것인가?

IR에는 현재 실행 중인 명령어가 들어가 있고 이후에는 다시 메모리에서 IR로 명령어를 가지고 와야한다. 이를 FETCH(패치)라고 한다.
이 패치를 하기 위해서 CU에서 제어버스를 통해 메모리에 READ 모드를 전달하고
PC에 담겨져 있는 정보를 주소버스를 통해 메모리에 전달하고 그 주소를 바타으로
데이터 버스를 통해 IR로 전달한다. 이 일련의 과정을 크게 패치라고 한다.

4. 인스트럭션 세트

  • Instruction set이란 CPU로 인식하여 실행할 수 있는 기계어이다.
  • 기본 디자인
    • 1바이트
    • 덧셈 & 뺼셈
      • 3bit(명령어) + 2bit (reg1) + 2bit(reg2) + 1byte
    • 곱셈 & 나눗셈
      • 3bit(명령어) + 2bit (reg1) ---> 이미 AX에 있는 값에 명렁어 + reg1을 하여 다시 AX로 이동
    • 메모리 접근 명령어
      • 3bit(명령어) + 2bit (reg1/ 메모리를 저장할 위치) + 3bit(데이터가 저장된 메모리 위치-- 직접 및 간접 접근 명령어로 나뉨)

이것을 통해 메모리에 저장된 기계어의 형태를 알아보고 CPU로 어떻게 불러오는지를 알 수 있다. 이제 메모리를 통해 우리가 입력한 정보가 어떻게 메모리에 저장되는지 공부한다

2019.03.12 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기.

  1. ASCII, Unicode, UTF-8에 대해 아는대로 상세히 이야기해라

character set

  • 문자 집합(charater을 모아둔 것)

charater encoding

  • 문자 인코딩 —> 부호화
  • 문자 집합을 메모리에 저장, 통신하기 위해 부호화 하는 방식

code point

  • 컴퓨터는 0,1 밖에 모르기 때문에 문자를 숫자화 해야한다.
  • a = 97이라고 했을 때 97을 코드 포인트라고 한다.(문자에 맵핑 된 정수)
  • 문자 하나에 정수 하나를 맵핑해 놓았다.
  • ex) ASCII(아스키) table
    • 0 부터 127까지 7비트를 사용하여 문자와 특수문자 등을 맵핑해놓음
    • 실제 저장은 1byte로 저장한다.

UNICODE(통합 코드)

  1. 기존에 아스키코드를 확장하여 16비트로 문자 표현 —> 65,536개 문자 가능
  2. 추가적으로 2byte자리 테이블을 17개 만들어 놓았다.
  3. 현재 4개의 table이 이용되고 있다.
  4. 유니코드 기본 평면에서 가장 첫번째 0000 ~ 0FFF는 아스키이다
  5. U+0000 ~ U+007F ---> 0000 0000 0111 1111 아스키를 첫 평면에 배치해놓았다.
  6. 한글은 어디 있나? U+AC00 ~ U+D7AF 파이썬에서 \uac00
hex(ord(“가”))
0xac00
hex(ord(“ㄱ”))
0x3131

유니코드 인코딩 방식

ASCII는 code point 그대로 컴퓨터에 저장된다.
unicode에서는'가' u+ac00 이라는 code테이블를 ac00 ---> 1010 1100 0000 0000을 그대로 메모리에 저장해 주면 정말 좋을 것 같다. 하지만 이렇게 되면 2번째 평면부터는 2바이트가 아니라 3바이트는 있어야 표현할 수 있다.
따라서 2byte + 4bit(17개 테이블을 나타내기 위한) 만 있으면 17개 다 표현할 수 있으나 저 4bit 떄문에 1byte가 더 필요하게 되고 3byte는 padding을 붙여 4byte를 관리해야 하게 된다. 이거 때문에 4byte를 쓰기에는 너무 아깝다.
특히 미국 입장에서는 "나는 평생 한글을 안 쓸 건데 1byte a,b,c등을 쓰기 위해서 4byte에 저장해야 한다고??”는 의문이 생길 수 있다. 그래서 나온 방법이 encoding 방식이다.
unicode는 테이블로 존재하며 '가'라는 애는 ac00 이라는 codepoint로 저장된다. uxac00을 메모리에 저장할 때 utf - 8 / utf-16/utf-32와 같은 방법들이 있다.
인코딩 방식은 크게 3가지로 나눌 수 있다. UTF-8/ UTF-16/ UTF -32 (Universal Coded Character Set + Transformation Format – 8, 16, 32-bit)에서 8 , 16 , 32는 bit를 이야기하며 각각의 숫자 비트만큼 늘어가며 확장된다. 각각을 구분해서 볼 때는 아스키코드들을 어떻게 저장할 것인가에 따라서 UTF-8, UTF-16, UTF-32로 나눌 수 있다.

UTF-16

utf - 16은 ac00을 1010 1100 0000 0000을 2byte로 그대로 쏜다. 1차 테이블(bmp, 1차평면)에 있는 문자은 그 숫자를 그대로 사용하고 문제가 발생하기 시작하는 2차 table에 있는 문자는 그냥 4byte로 쏜다. 기존 인코딩인 ASCII와 호환이 되지 않는 문제가 있다.

UTF-16은 저장될 때 little endian 방식으로 저장된다.

AC00 —> 00AC

UTF-32

utf-32은 모든 숫자를 다 4byte로 쏜다. 이게 제일 간단하다. 모든 문자를 편하게 쓸 수 있다.

UTF-32 역시 저장될 때 little endian 방식으로 저장된다.

00 ac 00 00 으로 저장

UTF-8

UTF-8은 1byte 기반이다. ASCII에 속하는 애들은 모두 1byte로 보낸다. 따라서 가변길이 인코딩이라고 부르며 1byte단위로 확장하며 문자를 표현한다. but UTF-8로 한글을 보내면 3byte로 된다. 따라서 우리는 UTF-16에 비해 1byte를 손해보게 된다. 현재 아스키코드를 그대로 읽을 수 있는 UTF-8이 기본이 되었다. 또한 UTF-8은 littel indian, big indian의 문제가 없다.

UTF - 8 인코딩 방식

utf-8
  • 1110xxxx의 111은 나타내는 바이트 수를 나타낸다. (3바이트)

컴퓨터 사이언스 부트캠프 with 파이썬: 3.1 UTF-8 - 1

little endian , big endian

전 세계 사람들은 1423을 보면 1423이라고 순서대로 읽는다. 여기서 가장 중요한 것은 1이다. most significant digit이다.사람이 보기에 제일 중요한 것 부터 저장하는 것은 big endian이다.
최근에 intel이 만든 것은 little endian이다. 1423 을 3241로 저장한다.메모리 저장 할 때는 메모리 단위로 하므로 ac/00 ---> 00/ac로 저장한다. 여기에서는 가장 뒤에 자리가 significant digit이다
network를 할 때 어떤 컴퓨터는 big인디언를 어떤 컴퓨터는 little인디언을 쓸텐데 어떻게 구분하는가? 따라서 network를 쏠 때는 big인디언으로 쓰기로 합의 하였다.

  • to_byte()함수를 이용하여 빅 엔디언 방식일 떄의 바이트의 나열과 리틀 엔디언일 때의 바이트 나열을 확인 할 수 있다.
a = 0x01020304   #16진수
a         #16909060 10진수 변환
a.to_byte(4, 'big)    #b'\x01\x02\x03\x04
a.to_byte(4, 'little)    #b'\x04\x03\x02\x01 

2019.03.10 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. 엡실론(epsilon)

먼저 엡실론을 파이썬에서
sys.float_info.epsilon이라고 서칭해보면
2.220446049250313e-16와 같은 값이 나온다. 굉장히 작은 숫자이다.

엡실론의 정의는 epsilon : difference between 1 and the next representable float

==> 1과 그 다음에 나올 수 있는 값의 차이를 이야기 한다.

sys.float_info.dig ----> 15 (digit 는 자리수)

그럼 여기서 말하는 15자리는 무엇인가?

먼저, 정밀도에 대해 소수점 아래 6자리 실수를 정밀도라고 적힌 책은 버려야 한다.(by teacher)

2진수 몇자리수로 10진수 1자리를 표현할 수 있을까?
10진수 1의 자리는 9까지 표현할 수 있으면 되는데 9는 ----> 1001(2) 이므로 4자리가 있으면 다 표현할 수 있다.
2진수 1111 채우면 10진수로 15까지 표현 가능 ----> 9보다 15가 크므로 1자리수는 완전히 커버 가능함
1111 111 7자리를 채우면 10진수 127까지 표현 가능 ----> 2자리수는 완벽히 커버가능
1111 1111 11 10자리를 채우면 10진수 1023까지 표현 가능 ----> 3자리 완벽히 커버가능

floating에서 가수부는 총 23bit에 기본 1.0 1bit를 더해 총 24bit 까지 커버가능한데.
과연 1111 1111 1111 1111 ----- 24까지 했을 때 어디까지 커버가 가능할까?

10진수로 나타내면 16,777,215 이다. 즉 완벽히 커버 가능한 자리는 9.999.999 총 7자리에 대해 커버가 가능한다. 이는 6 ~ 7 자리를 이야기 할 수 있고 10진수로 7자리를 표현할 수 있다는 정밀도가 있는 것이다.

double에서는 mantisadigit - 53이다.
1111 1111 1111 1111----> 53개까지 했을 때 어디까지 커버가 가능할까?
9,007,199,254,740,991 ---> 총 15자리까지 완벽 커버가능하다.

여기서 제일 마지막 1은 더 이상 커져도 신뢰할 수 없다.

엡실론

실수 1.0 다음 수에 대해 생각해 보자.

floating에서
1.0 x 2 ** 0 그 다음은 1.00000000000000000000001(23자리 뒤의 1) x 2 ** 0 이고
따라서 차이는 1.0 x 2 ** -23로 나타낼 수 있다.

double은 1.0x 2 ** -52
sys.float_info.epsilon == 2.0**-52 해보면 True가 나오게 된다.

double로 변하면서 정밀도가 훨씬 좋아져 epsilon의 값도 많이 작아지게 된다.

epsilon 컴페어리즘

그럼 이 엡실론을 어디에 쓰일 수 있을까??
float a = 10.5 다음에 쓰일 수 있는 수 사이의 차이를 구해보자.

10.5 = 8 + 2 + 0.5 = 2 ** 3 + 2 ** 1 + 2 * -1 = 1.0101(2) x 2 ** 3
diff = 2 ** e x epsilon( 2 ** -23)
2 ** 3 x 2 ** -23 = 2 ** -20의 간격을 두고 다음 수가 쓰일 수 있다.

하지만 이렇게 하면 너무 시간이 오래 걸린다.

엡실론 계산

매번 2진수로 바꾸고 지수부를 구하기에는 시간이 너무 오래 걸리므로 지수부보다 num은 무조건
크므로 지수부 대신 num을 쓰게 되면 사이 간격을 나타내는데 더 보수적으로 쓰게 되는 것이다.

하지만 여기서는 더 보수적으로 잡아서 표현범위를 나타내는 지수부를 그대로 num으로 근사시켜 사용한다.
이렇게 되게 되면 우리는 너무나도 쉽게 다음에 쓰일 수와의 차이를 알 수 있다.

실수 비교

따라서 실수는 if(a==b)처럼 직접 비교하면 안된다.

그러므로 a,b를 비교하는 함수를 작성해 비교한다. (오히려 배우고 나니 문제가 더 커진 기분이다.)

def is.equal(a,b):
    return True of False

여기서 실수 비교 기법 2가지가 나온다.

  • 절대 비교 기법
def is_equal(a,b):
    return |a-b| <= 1e-10    

두 수의 차이가 1e - 10 보다 작다면 같다고 생각해도 무방하다.
하지만 1e - 10을 어떻게 정할 것인가? 함수를 만들어 놓은 이유는 계속해서 쓰기 위해서 인데
이렇게 해놓으면 매번 함수의 구현을 바꾸어야 하므로 매번 내부함수를 바꿔야 하므로 추상화에 위반된다.

from math import fabs
def is_equal(a,b):
    return fabs(a-b)<= 1.0e-10    #fabs는 절대값을 나타내는 math모듈에 있는 함수??이다.

if is_equal(a,b):
    print("이 정도 차이면 봐줄게")
else:
    print("이건 같은 수가 아니징")

따라서 우리는 매번 1e - 10을 필요에 따라 바꾸어야 하는 문제가 발생한다.
1e - 10을 수학적으로 족보를 가진 애로 만들어야겠다.

  • 상대 비교 기법

relative Error = fabs(a-b)/max(fabs(a). fabs(b)) max는 둘중에 큰수
분수로 표시하는 이유는 5.5와 5.3의 차이 그리고 55와 53의 차이를 똑같게 만들어 준다.

from math import fabs
import sys
def is_equal(a, b, w=0):

    """
    is_equal(a, b, w = 0) ---> bool

    w는 가중치입니다. w를 조정함으로서 diff의 범위를 조정할 수 있다.

    w를 0부터 늘려가며 상대 오차 범위를 조정해주세요    

    """
    ep = sys.float_info.epsilon
    diff=fabs(a-b)    
    return diff <= max(fabs(a), fabs(b))*ep*(2**w) #큰 값을 하는 이유는 큰수를 해서 조금 더 오차를 인정해주자 그런 느낌으로 보면 된다.

위와 같은 것을 epsilon 컴페어리즘이라고 한다.

즉 오차범위를 어디까지 허용할지 내가 직접 함수를 만들어서 가중치를 조절해가며 쓸 수 있는 것이다.

원래
a = 3.0
b = 1.0 * 3
a == b 하면 false가 나오나

위의 값에 비교해보면

if is_equal(a,b):

        print('things')

things

가 나오게 된다.

2019.03.10 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. 실수

  2. 고정 소수점

고정소수점(Fixed Point) 방식은 소수점 이상, 소수점 이하를 특정 비트로 딱 나눠서 처리하는 것을 말한다.
만약 반반씩 하기로 하면 4바이트 즉 32비트로 처리하면 정수부분은 16비트 소수 부분도 16비트로 표현하는 것이다.
아니면 1바이트는 정수부분(8bit) 3바이트는 소수부분(24bit)로 표현하기도 한다.
하지만 32비트가 표현할수 있는건 한계가 있어 정수부분과 소수부분을 어떻게 나누어 표현하느냐의 문제가 발생한다.

실제 함수를 작성하면서 정수부분은 거의 범위가 적고 소수자리는 길게 필요하다면 소수부분에 비트를 많이 할당할 수 있다.
고정 소수점은 정밀도는 높았으나 표현 범위가 낮다.

  1. 부동소수점(Float Point)

부동소수점은 정밀도는 낮으나 표현범위 넓다. 따라서 어떻게 정밀도를 핸들링 할 것인가가 관건이다.
위에 고정소수점은 소수점 위치를 정하고 그이상 그 이하 나눠서 처리 했으니
이번엔 소수점 위치가 계속 바뀌는건가 라고 생각하시겠지만 아니다.

컴퓨터 언어에서 쓰이는 실수형 자료형(float이나 double)은 보통 부동소수점 방식을 쓴다.
근데 이 규격은 IEEE에서 정하고 있다. (여기서 쓰이는건 IEEE 754 입니다.)

IEEE 754는 아래 그림과 같은 방식으로 표현됩니다.

부동소수점

1023 ----> 102.3 x 10 / 10.23 x 10 ** 2 / 1.023 x 10 ** 3 / 0.1023 x 10 ** 4 와 같이 다양한 형태로 나타낼 수 있다.

여기서 floating point가 나오는데
floating 은 둥둥 떠다닌다를 뜻한다 이 말은 소수점의 위치가 언제든지 옴겨질 수 있는 것을 말한다.

부동소수점

부동소수점은 단정도 부동소수점과 배정도 부동소수점으로 나누어 진다.

  1. 단정도 (single precision)

    • 32 bit (4byte)
    • 부호(1bit) + 지수(8bit) + 가수(23bit)
    • 지수부를 넓힌다는 건 표현범위를 넓힌다는 것이다. 가수부를 넓힌다는 건 정밀도를 넓힌다는 것이다.
  2. 배정도(double precision)

    • 64 bit (8byte)
    • 부호(1bit) + 지수(11bit) + 가수(52bit) #단정도에 비해 정밀도가 거이 2배이고 표현범위도 늘어남 #단 메모리를 많이 쓰게 됨. 따라서 double을 되도록 안쓰고 싶어한다??

파이썬은 기본적으로 배정도를 사용한다.

import sys

sys.float_info를 해보면

배정도

여기서 가수부를 뜻하는 mant_dig을 볼 수 있는데 여기서 53이 되어있는 것을 볼 수 있다.
이는 파이썬이 배정도를 사용한다는 것을 알 수 있음과 동시에 배정도는 가수부가 52bit인데 왜 53bit가 할당되어 있는지에 대한 의문저 역시 품게 된다.

이를 해결하기 위해서는 정규화에 대해 알아야 한다.

5234 -> 523.4 x 10*1

     -> 52.34 x 10** 2

     -> 5.234 x 10 ** 3  

이중에서 무엇을 메모리에 저장할 것인가? 이 수를 정규화시키고 저장한다.

정규화란? 정수 부분이 0이 아닌 1자리 자연수로 만드는 것이다.

110.1(2) ---> 1.101 x 2 ** 2

0.0000111 ---> 1.11 x 2 ** -5 ---> 여기서 1.11을 가수부 2 ** 5는 지수부 2는 기수라고 한다.

따라서 2진수에는 제일 앞이 무조건 0이 아니라 1이 되게 된다.( 1자리 자연수)

+- 1.man x 2 ** ---> 여기서 1. 은 멘티사(가수)이지만 따로 저장하지 않는다.(항상 1이 나오므로)
따라서 1.은 따로 저장하지 않으면 더 좋다.

1.010101 x 2 ** 4 일 때 저장은 010101(가수) 이후에 000000000000 으로 나머지 가수부분을 채운다.

따라서 파이썬에서는 man_dig가 1.을 포함하므로 53개로 나타남(왜냐하면 1도 실제로 가수부이므로)

  • 이런 일로 인해서 실제 메모리에 저장되는 것과 파이썬에 나타나는 것과 차이가 나타나게 된다.

다음으로 넘어가서 2 ** 5 에서 전체는 지수부라고 표현하고 2는 기수 5는 exponent(제곱) real 이라고 부른다.
여기서 exponent real은 exp - bias로 구할 수 있다.

exp-bias

여기서 bias는 상수인데 bias를 알면 exponent를 구할 수 있다.

bias

따라서 float에서는 지수가 항상 8자리의 비트를 가지므로 bias는 127이다.

Ere = Emem - bias
Emem = Ere + bias ----> 5 + 127 = 132 를 2진수로 바꾼게 Emem 가 된다.

이제 예를 들어 실행해보자

만약에 특정 변수에 10.625라고 변수를 할당해주면 과연 10.625는 어떻게 메모리에 저장될까?

먼저 10.625로 2진수로 바꾸어야 한다.
10.625 = 8 + 2 + 0.5+ 0.125 (현실에서는 이것보다 복잡하는 2진수로 바꾸는게 쉽지 않다)
= 1010.101(2)

이 숫자는 정규화되어 1.010101(2) x 2 ** 3으로 나타난다.

앞에 가수부의 1.을 제외한 010101이 가수부에 저장되고 (가수부는 52자리를 지원하므로 나머지 부분은 모두 0으로 저장)

Ere = Emem - bias 이므로 Emem = Ere + bias 이다.

따라서 지수부를 나타내는 Emem = 3 + 127 이므로 130 이다.

130을 2진법 수로 나타내면 128 + 2 ----> 2 ** 7 + 2 ----> 10000010 이 된다.

따라서 명확하게 적어보면

0(sign) 10000010 (지수부) 01010100000000000----(가수부 52자리)로 적을 수 있다.

그렇다면 파이썬에서 0.01을 100번 더했을 때 1이 나오지 않는 이유는 무엇일까?

a = 0.01
result = 0.0
for i in range(100):
    result += a

result #1.0000000000000007

이는 0.01을 2진법 수로 바꾸었을 때 가수부를 나타내는 자리수가 부족했음을 알 수 있다. 그 숫자의 차이는 미비하나 100번이나 더 해주었을 때 어느정도의 차이를 만들어낸다.

따라서 정밀도에 대한 문제를 부동소수점에서는 꼭 다루어야 하는데 그것의 시작이 바로 옙실론이다.

옙실론은 다음에서 이어서 설명한다!

2019.03.10 TIL

(TIL은 스스로 이해한 것을 바탕으로 정리한 것으로 오류가 있을 수 있습니다)

# 질문에 답하기

  1. 양의 정수 표현 방법
  2. 음의 정수 표현 방법

정수 표현

양의 정수 표현

  1. 부호비트는 0 (제일 앞에 숫자) 양수는 0 , 음수는 1
  2. 정수의 2진수로 표현 나머지는 0
  • 부호비트가 존재해서 양수와 음수를 구분해 준다고 하는데 음의 정수 표현이 왜 다시 나오며, 보수라는 개념이 왜 나올까? 아니 차라리 음의 정수 표현과 보수가 이해가 되는데, 양의 정수 표현 방법에 제일 앞에 부호비트를 표시한다는 내용이 왜 나오는지 이해가 안된다.
  • -> 보수를 하게 되면 자연스럽에 양수를 표현하였던 부분 0 이 1로 되면서 위의 조건에 적합하게 된다.

43을 1바이트로 표현하면

43 = 0010 1011(2) 이므로 0x2b (메모리 주소)(메모리에 저장되는 형태)
-43은 1010 1011(2) 이면 엄청 행복할텐데... 그렇지가 않다.
따라서 음의 정수에 대해서 더 공부해야 한다.

음의 정수 표현

음의 정수를 시작하기 전에는 보수의 개념이 필요하다.
보수란 쉽게 말해 보충해주는 수이다. 예를 들어 10진수에서 9의 보수를 구한다고 가정해보자. 3의 9의 보수는 3을 더해 9가 되는 6이다. 26의 9의 보수는 73이다. (보수는 각 자리수 별로 계산한다)
보수를 구하는 것은 각각의 자리수에 특정 수를 빼면 특정수에 대한 보수를 구할 수 있다.
123의 9의 보수는 876이다.
그럼 123의 10의 보수는 9의 보수에 +1 을 해주면 된다.(10진수이기 때문에) 877이다.

그럼 2진수의 보수에 대해서 알아보자.

0000 0100 은 10의 자리로 4를 나타낸다. 이러한 4의 1의 보수를 구해보면
1111 1011 이다. ( 2진수이기 때문에 각 자리수를 빼면 된다.)
여기에 1을 더하면 2의 보수가 표시된다.(2진수이기 때문에 / 1111 1100)
이러한 보수를 통해 - 뺴기가 따로 없는 컴퓨터에서 계산을 할 수 있다. 9 - 4 ===> 9 + (-4)로 계산하게 된다.

  • 10의 보수는 9의 보수를 구하여 +1을 해주면 되고
  • 2의 보수는 1의 보수를 구하여 +1을 해주면 된다.

이러한 보수로 음의 정수를 표현하는 이유는

  1. 1byte를 기준으로 볼 떄 제일 앞자리를 부호를 결정하는 것으로 보면 1비트를 손해보게 되고 또 2진수 계산에서 있어서 매우 귀찮게 된다.
  2. 컴퓨터가 인식할 수 있는 것은 on, off 밖에 없다. 따라서 컴퓨터에는 -라는 개념이 없고 컴퓨터는 모든 작업을 가산기로 처리한다. (가감기가 없음) 따라서 9-4를 9 마이너스 4로 계산하는 것이 아니라 9 플러서 -4로 가산 방법으로 처리한다.

컴퓨터의 덧셈, 뺼셈 계산 방법

이걸 바탕으로 9 -4 를 다시 해보면

  0000 1001 (9)        
+ 1111 1100 (-4)       
1 0000 0101 ---> 제일 앞 받아올림수는 버리고 최종적으로 0000 0101 로 5가 나오게 된다.

참고하면 좋을 자료

음의 정수

출처 : 2의 보수법으로 음수 표현하기

컴퓨터의 곱셈, 나눗셈 계산 방법

곱셈
그렇다면 과연 곱셈과 나눗셈은 어떻게 이루어질까?

4bit 연산에 맞추어 곱셈을 해보자.

10(10) 와 5(10)의 곱셈을 예로 들어서 진행해보자.

먼저 10을 2진법으로 바꾸면 8 + 2 ---> 1010(2)

5를 2진법으로 바꾸면 4 + 1 ----> 0101(2) 이 된다.

정수 곱셈
    0000 | 0101 ---> 처음에 주어진 수를 0000으로 리셋시키고 곱하는 수를 오른쪽에 적는다. 

  + 1010        ---> 위의 0101에서 제일 오른쪽의 수가 1이므로 왼쪽에 1010을 더해준다.

    1010 | 0101 ---> 여기까지의 과정을 거친뒤 shift 오른쪽으로 한번 진행  (shift 1)

    0101 | 0010 ---> 오른쪽으로 shift를 거친뒤에는 제일 앞에 0을 적어준다. 

                ---> 제일 오른쪽의 숫자가 0이므로 바로 shift 2진행

    0010 | 1001 ---> 제일 뒤의 숫자가 1이므로 + 1010을 진행해준다.

  + 1010        

    1100 | 1001  ----> 이후에 shift 1회 진행 shift 3

    0110 | 0100 -----> 제일 마지막 숫자가 0이므로 shift 4 진행

    0011 | 0010 -----> 제일 앞의 00을 빼고 110010(2)이 답이 된다.
  • 식에서 볼 수 있는 0101을 우리가 곱하기 하는 것처럼 제일 뒤에 1부터 한칸씩 옴겨가며(shift) 계산해 준다고 생각하면 된다.

왜 곱셈은 오른쪽으로 shift하고 나눗셈은 왼쪽으로 shift를 진행할까??

  • 우리가 곱셈을 할 때는 곱해주는 숫자를 1의 자리부터 시작하여 10의자리 100의 자리 옴겨가며 곱해주고 나눗셈을 할 때는 가장 높은 숫자부터 확인하며 나누어 주기 떄문에 연관하여 생각해 보면 된다.

나눗셈
나눗셈은 어떻게 진행될까? 정리해보자.

그럼 이번에는

11 / 3을 해보려고 한다.

먼저 11을 2진수로 바꾸면 1011(2)

3을 2진수로 바꾸면 0011(2) 이다.

정수 나눗셈

나눗셈은 정말 이해가 잘 되지 않는데

선생님 말로는 0011의 00을 빼고 11로 똑같이 나눗셈을 한다고 생각하면 된다고 했다.

?가 들어가는 곳은 위에 자리수가 0 , 0 으로 되는 것을 이야기 했다.

어떻게 이루어 지는 것을 확인하고 이후에 또 해볼 수 있을 것 같다.

나눗셈을 잘 설명한 영상이 있어서 첨부한다.

CPU의 나눗셈 - 개발자가 꼭 알아야 할 컴퓨터 하드웨어

+ Recent posts