※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)


몇 번 말했던 것 같다. 요즘은 정말 포스팅 하는 재미에 산다. 으하하 오늘도 재밌는 내용을 가져왔으니 독자분께서는(계...계시나요?) 잘 읽어주세요.

지금 내가 공부하고 있는 책은 Computer Systems : A Programmer's Perspective이다. 프로그래머  관점에서의 컴퓨터 시스템이다. 그렇다. 오늘은 프로그램을 돌려볼 것이다. 예전에 교수님께서 최적화해보라고 내주셨던 과제 프로그램도 있지만 그것보다는 책에 있는 내용을 가지고 예를 들며 설명해보려고 한다. 

이번 포스팅에서 아래의 프로그램을 최대한 개선해보고자 한다.



위 프로그램을 따로 설명하지는 않겠다. 다들 C코드는 어느 정도 이해할 수 있을 것이라고 가정하고 가자. 아래의 코드를 참고하여 알아서 이해해보도록 하자.



이 프로그램을 실행하면 대략 100000us 정도의 시간이 소요된다. 상당히 느리다. 


우리가 개선할 부분은 바로 위에 있는 combine이라는 함수이다. 우선 필요없는 함수 호출을 줄여보자. 가장 눈에 띄는 것은 for loop에서의 vec_length(v)이다. 이는 v라는 구조체의 길이를 리턴해주는 함수로 항상 같은 값을 리턴하게 되어있다. 앞에서 procedure call에서 아주 자세하게 설명했듯이 함수를 호출하는 것은 CPU입장에서 굉장히 많은 시간을 소모하는 일이다. 따라서 없애는 것이 현명하다. 어짜피 같은 값을 리턴하므로 아래와 같이 vec_length(v)값을 length라는 integer 변수에 뽑아내자.



이와 같이 개선하면 100000us가 80000us정도로 개선이 된다. 대략 20%정도 개선된 것이다. 함수 호출을 줄인 효과이다. 위의 코드와 같이 length라는 변수에 접어넣는다면 assembly code에서는 그냥 단순히 register에 값을 집어넣고 접근하면 되기 때문에 빠른 접근이 가능해진다.


그 다음에 개선할 부분은 다음과 같다. for loop 안의 get_vec_element(v, i, &val) 함수이다. 여기에서도 쓸데 없이 계속 함수를 호출하고 있다. 어떻게 이 함수를 안 쓰고 할 수 있냐고? 배열이다. 저 함수는 v라는 구조체에 선언되어있는 v->data의 i번째 값을 val이라는 변수에 저장하라는 함수이다. 이것을 위해서 함수를 호출해야 하고 함수 내에서는 memory access를 도대체 몇번을 해야하는가? 그것이 비효율적이라는 것이다. 따라서, 우리는 새로운 함수 하나를 선언하겠다.



이 함수는 v라는 구조체의 data의 주소를 return하는 함수가 되겠다. 그 값이 무엇인가? 바로 v->data 의 시작 주소이다. 이 부분에 대해서 내가 따로 블로깅을 해두진 않았지만, v->data[i]의 값을 접근하기 위해서는 v->data의 시작 주소를 알고 그 값을 기준으로 한 칸씩 이동하는 것이다. 아래와 같다. 



이와 같이 v->data의 시작 주소를 알았으니 이 값을 가지고 마음껏 memory 연산을 통해 값을 접근하면 된다. 아직 memory 접근을 해야하지만 적어도 함수 호출은 없앴다. 개선된 combine함수는 아래와 같다.


get_vec_start(v) 함수로 얻은 data 값을 가지고 data[i]와 같이 마음껏 접근할 수 있다. 이렇게 해주면 35000us와 같이 절반 이상의 성능 개선을 볼 수 있다. 오 엄청난 개선이다. 여기서 또 어떻게 개선할까? for 문 안을 봐서 아직 한 번도 손대지 않은 메모리 접근을 찾아보자. *dest이다. *dest로 되어있기 때문에 매번 loop를 돌 때마다 메모리 연산을 해줘야 하는데, 이 시간이 또 우리는 싫다는 것이다. *dest 처음 값을 변수에 저장해놓자. 아래의 코드를 보자. 



우와 엄청난 개선이 이루어질 것 같다. 그런데 막상 프로그램을 실행시켜보면 35000us로 성능 개선이 보이지 않았다. 학교에서 배웠던 것을 생각해보면 여기에서 60%정도의 성능개선이 나타난다고 했다. 그런데 왜 나타나지 않을까?


이럴 때를 위해서 우리는 어셈블리어를 공부했다. (이유는 무엇이든 좋다.) 바로 위의 코드를 combine4로 하고 그 위에 있는 코드를 combine3라고 하고 아래의 그림을 봐보자.


combine3에 대한 어셈블리코드는 아래와 같다.



여기서 보면 L9 영역이 바로 for문 안의 부분이다. 메모리 연산 개수를 보면 8개이다. 10개 중에 8개가 메모리 연산이다. 조금 더 구체적으로 볼까? *dest 값을 얻기 위해 어떤 instruction을 수행했는지 살펴보자. 


movl        12(%ebp), %eax

movl        (%eax), %edx


이 두 명령어가 *dest를 얻는 부분으로 매 loop마다 반복된다. 그리고 data[i]를 얻는 부분은 아래와 같다.


movl        -12(%ebp), %eax

sall         $2, %eax

addl        -20(%ebp), %eax

movl        (%eax), %eax


위의 네 줄이 data[i]를 얻는 과정이다 그렇게 해서 얻어진 *dest 값을 갖는 %edx값과 data[i] 값을 갖는 %eax 값을 imull instruction으로 곱하는 것이다.


imull        %eax, %edx


위와 같이 곱한다. 자 대충 combine3에 대한 어셈블리 코드가 이해가 되는가? 이제는 combine4에 대한 어셈블리 코드를 보자.



마찬가지로 .L9이 for 문 안쪽의 부분이다. 한번 분석해보자. 우선 acc값을 어떻게 얻는가?


movl        -16(%ebp), %edx


아까 combine3에서는 두 개의 메모리 계산을 했던 것에 반해 이번에는 한 개의 메모리 연산으로 acc값을 얻어 올 수 있었다. data[i] 값은 어떻게 얻는지 살펴보자. 사실 이 부분은 별로 다를 것이 없다.


movl        -12(%ebp), %eax

sall         $2, %eax

addl        -24(%ebp), %eax

movl        (%eax), %eax


%ebp에 대한 offset이 조금 바뀌긴 했지만 같다고 보면 된다. 자 이제 대충 왜 효율 개선이 없었는지 알겠지? 어셈블리언어, 컴퓨터가 이해하는 언어 입장에서는 바뀐게 거의 없기 때문이다. 사람이 머리 속으로 이해하기에는 편하겠지. 하지만 명령어를 실행하는 것은 사람이 아니라 컴퓨터라는 점을 명심하자.


지금까지 한 일을 한 마디로 정리해보자. 바로 메모리 연산을 줄이고 함수 호출을 줄임으로서 프로그램 성능의 개선을 보는 것이다. 이러한 방식으로 성능을 개선하는 것은 하드웨어에 독립적으로(hardware independent) 개선하는 것이다. 즉, 어떠한 하드웨어 환경에서도 개선 효과를 볼 수 있는 general 한 개선 방법이다. 다음 포스팅에서는 hardware dependent 한 성능 개선을 찾아보자.

Posted by 빛나유

댓글을 달아 주세요

  1. 성현구 2015.10.14 22:33  댓글주소  수정/삭제  댓글쓰기

    좋은글 잘 읽고 갑니다~

※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)


OS의 File System에서 Protection을 설명하기 전에 Consistency에 대해서 이야기 해보자. 두 개의 Process가 있다고 하자. P1과 P2이다. 이 두 프로세스가 하나의 파일을 open 한다고 해보자. 




이 파일을 접근하는데 P1이 Abcd를 가나다라로 바꾸고 있다고 가정하자. 아직 파일은 저장되지 않은 상태이다. 이 때 P2가 이 파일을 읽으면 가나다라로 바뀌지 않고 Abcd로 나타나있을 것이다. 이는 Consistency에 어긋난다. 이것을 위해서 OS에서 제공하는 것이 Lock이다. 말 그대로 하나의 프로세스가 특정 파일을 건드리고 있을 때는 다른 프로세스는 그 파일에 접근할 수 없게 하는 것이다. Lock도 여러가지 방법으로 구현할 수 있다. 가장 대표적으로 쓰이는 두 가지가 바로 Unix semantics와 Session semantics이다. 


Unix semantics는 하나의 프로세스가 열린 상태에서 Read 혹은 Write를 할 때마다 Lock과 Unlock을 되풀이 하는 것이다. 즉, 한번 open()를 통해 파일을 열고 난 후 read를 하면 read가 끝날 때까지 lock을 걸어서 다른 프로세스는 그 파일을 건들지 못 하게 하는 것이다. 


Session semantics는 더욱 더 확실한 방법의 consistency를 보장한다. 프로세스가 파일을 접근하는 과정은 대략 open한 후 read 혹은 write를 원하는 만큼 실행한 후 close 하는 것이다. Session semantics는 프로세스가 파일을 open하고 close할 때까지 Lock을 한다는 것이다. Consistency가 확실하게 보장될 것으로 기대된다. 


이제 본격적으로 File Protection에 대해서 이야기 해보자. File Protection은 기본적으로 Multi-user system에서 중요하게 다루어진다. 하나의 Unix 서버에 A라는 사용자와 B라는 사용자가 있다. A가 그 시스템에 자기의 일기를 써놨다고 하자. B가 그걸 읽어도 될까? 혹은 수정해도 될까? 더욱이 안된다. 이러한 이유에 때문에 Multi-user system에서는 File Protection이 구현되어야 한다. 


그렇다면 어떠한 부분에 초점을 맞춰서 protection이 구현되는 것인가? 바로 실행하는 함수들에 초점을 맞추어서 Protection이 구현된다. Read, Write, Execute가 바로 그것이다. 허가되지 않는 사용자가 내 파일을 read, write, execute하지 못 하도록 해보자는 것이다.


이것을 구현하는 방법 중에 가장 많이 사용되는 방법이 Access Matrix이다. 하나의 테이블에 access 권한을 기록해두는 것이다.



위의 표에서 말하는 도메인은 사용자를 의미하고 Object는 각각의 파일들을 의미한다. 자 생각해보자. 뭔가 이상하지 않나? 하나의 시스템에 도대체 파일이 몇 개가 있는데 이런 식으로 관리하겠다는 것인가? 수천개 혹은 수만개 수억개까지도 가능한데 그 모든 파일에 대해서 저런 식으로 테이블을 가지고 있겠다는 것은 어찌보면 비현실적이다


결론적으로 말하면, Unix 시스템에서는 그럼에도 불구하고 저런 식으로 사용을 하기는 한다. 그런데 진짜 저런 식으로 사용하는 것이 아니고, 위의 데이터에서 필요한 데이터들만 저장하여 갖고 있는다. 여기서 Sparse Matrix를 이야기하면 될 것 같다. 위의 테이블을 보면 내용이 있는 칸보다는 없는 칸이 더 많다. 아무 내용도 없는데 굳이 공간을 할당하고 있을 필요가 있을까? 아니다. 그래서 다음과 같이 기록하는 것이다.


도메인 단위로 다음과 같이 기록해둘 수 있다. 이렇게 도메인 기준으로 나타낸 것을 Access list 방식이라고 한다.


D1 : (F1, R), (F3, R)

D2 : (Printer, Print)

D3 : (F2, R), (F3, X)

D4 : (F1, RW), (F3, RW)


필요한 내용들만 뽑아서 쓴 것이라 훨씬 사이즈가 compact해질 수 있다. 그런데 Domain을 기준으로 하지 않고 Object를 기준으로 해도 될 것 같지? 그렇다. 해도 된다.


F1 : (D1, R), (D4, RW)

F2 : (D3, R)

F3 : (D1, R), (D2, X), (D3, RW)

Print : (D2, Print)


위와 같이 해도 좋다. 이와 같이 Object 기준으로 나열해둔 것을 Capability list 라고 한다. Unix system은 그렇다면 Access List 또는 Capability list 둘 중에 어떤 방법을 사용할까? 이번 포스팅이 끝날 때 쯤이면 이 답을 낼 수가 있다. 답을 찾아서 슝슝슝 가보자.


Unix에서 ls -l을 실행하면 어떤 결과를 얻는가? 파일의 inode 정보를 얻을 수 있다. 당연히 거기에는 rwxrwxrwx와 같이 실행권한을 의미하는 부분도 포함되어있다. (이 부분이 무엇인지 모르는 분은 따로 인터넷에 검색해보시길 바란다. 매우매우 쉽게 찾을 수 있을 것이다.) 말하고 싶은 것은 Unix 시스템에 존재하는 모든 파일은 inode에 실행권한 부분을 가지고 있다는 것이다.


또 다른 이야기를 해보자. 이전 포스팅에서도 다루었던 파일을 접근하는 방법에 대한 이야기 이다. 그런데 이번에는 권한에 초점을 맞추어서 이야기를 해볼 생각이다.


fd1 = open("/etc/passwd", "O_RDONLY");

fd2 = open("/anything/abc/test.txt", "O_RDWR");

fd3 = open("/etc/passwd", "O_WRONLY");



파일을 open할 때 어떤 과정으로 하는지 다시 한번 되세겨 보자.

1. 파일 이름을 얻는다.

2. 디렉토리 검색을 한다.

3. 파일이 검색될 경우 metadata를 메모리에 로딩한다.

4. metadata가 있는 주소의 위치를 가리키는 포인터를 system-wide open-file table에 저장한다.

5. system-wide open-file table index의 위치를 per-process open-file table에 저장한다.

6. per-process open-file table index의 주소를 return 한다.


여기서 3번과 4번을 보자. 3번에서는 metada를 로딩을 한다고 했는데, 추가적으로 한 가지 더 하는 것이 있다. 바로 여기서 권한을 검사한다. "너가 /etc/passwd에 접근할 권한을 갖고 있는 놈이냐?"하고 물어보는 것이다. 검사를 할 수 있냐고? 당연하다. 왜냐하면 metadata에 rwxrwxrwx와 같은 권한 정보가 있으니까. 그리고 4번에서는 metadata의 주소가 system-wide open-file table에 저장하는 일을 하는데 이 때 system-wide open-file table을 잘 보자. right이라는 field가 있다. 권한이다. 즉 이 테이블은 모든 열려있는 파일들에 대한 권한을 다 가지고 있다. 여기서 또 검사를 하는 것이다. 가령 위의 C코드를 봤을 때 fd1은 /etc/passwd 파일을 ReadOnly로 오픈했다. 만일 fd1 포인터를 통해 /etc/passwd 파일을 write하려고 한다면 어떻게 될까? 권한이 없어서 불가능하게 된다. 아래의 그림과 같다.



자, 이제 다시 위의 질문으로 돌아가보자. Unix 시스템에서는 Access List 방식을 사용하는가? Capability List 방식을 사용하는가? 정답은 "둘 다 사용한다" 이다. inode table에 저장되어 있는 rwxrwxrwx와 같은 정보들이 바로 access list를 구현해놓은 것이고, system-wide open-file table에 저장되어있는 각 파일 포인터에 대한 접근 권한이 바로 capability list를 구현해놓은 것이다. 


OS에 대한 포스팅은 다음에 또 언제 하게 될지 모르겠다. 하지만 내가 공부하는데로 바로바로 올릴 것에 대해서 약속합니다. 


'Operating Systems' 카테고리의 다른 글

[OS] Memory Management Scheme  (8) 2013.06.30
[OS] File System (Protection)  (1) 2013.05.18
[OS] File System  (0) 2013.05.18
[OS] File System  (4) 2013.05.17
[OS] Deadlock Detection  (2) 2013.02.03
[OS] Deadlock Avoidance (Banker's Algorithm)  (5) 2013.02.03
Posted by 빛나유

댓글을 달아 주세요

  1. 행인 2013.12.15 01:37  댓글주소  수정/삭제  댓글쓰기

    도메인 기준으로 나타낸것이 capability list이고
    파일 기준으로 나타낸것이 access list인것 같은데 반대로 써놓으신것같네요

[OS] File System

Operating Systems 2013. 5. 18. 01:07

※ 질문/내용오류/공유할 내용이 있다면 jinkilee73@gmail.com으로 메일 주세요 :-)


File System 구조에 대해서 이야기 해보자. 디스크가 어떤 구조로 이루어져 있을까? 보통 우리들은 하드디스크를 여러 개의 파티션(C드라이브, D드라이브...)등으로 나누어서 사용하고 있다. 이런 식으로 나누어서 사용하는 것을 Partition한다고 한다. 이렇게 파티션된 각각이 파일 시스템이다. 파티션은 해도 되고 안 해도 되는 것이다. 그림으로 보자.



하나의 디스크를 두개로 나누어서 파티션 A, 파티션 B로 쓸 수도 있고 두 개의 디스크를 하나의 파일 시스템으로 묶어서 쓸 수 있다.


위 사진에서 하나의 파티션을 보자. 하나의 파티션은 디렉토리와 파일 부분으로 나뉘어 있다. 그렇다. 우리가 머리 속에 가지고 있는 트리 구조의 디렉토리는 사실 파일 시스템의 일부분에 데이터로 저장되어 있는 형태를 띈다. 


요즘 디렉토리 구조는 보통 트리 구조를 사용한다. Flat directory structure, 2-level directory structure 등도 있지만 현실적이지 않기 때문에 설명하지 않고 자주 쓰이는 트리구조에 대해서 이야기 해보련다.


트리 구조는 예상하다시피 다음과 같다.



각각의 디렉토리들이 트리 구조를 이루고 있다. 여기서 질문 하나!! 링크는 어떻게 구현한 것일까? 링크는 다들 알다 시피, 윈도우즈로 따지면 "바로가기 아이콘" 같은 것이다. 위의 그래프를 기반으로 했을 때 제일 오른쪽에 있는 파일 A가 젤 왼쪽에 있는 파일 B에 대한 링크(바로가기)라고 한다면 어떻게 되야할까? 상식적으로 생각했을 때, 그냥 선이 이어져 있으면 될 것 같다. 그렇다. 그냥 선만 이으면 된다.



이와 같은 그래프를 Acyclic graph라고 한다. 잠깐 자료구조 이야기를 하면, 위에 위에 그림에서는 트리라고 했고 위의 그림에서는 Acyclic graph라고 했는데, 그래프라고 한 이유가 있다. 즉, 위의 구조는 트리 구조는 아니기 때문이다. 그런데 왜 아닌가.. 그건 잘 모르겠다. 자료구조 이야기이므로 지금은 잠깐 패스하자. Acyclic이라고 한 이유는 루프가 없기 때문이다. 그런데 로프가 존재하는 파일 디렉토리 구조도 있다. 



링크에 대해서 잠깐만 이야기 해보자. 하드링크와 소프트링크가 있다는 것은 어느 정도 들어봤으리라고 생각한다. 각각이 무엇을 의미할까? 하드링크는 하나의 파일 시스템 내에서의 링크를 의미하고 소프트링크는 서로 다른 파일 시스템에 존재하는 파일끼리도 링킹을 할 수 있게 하는 것을 말한다. (하드 링크가 훨씬 빠르다고 한다.)


파일을 링크를 통해서 연결을 하면 어떤 현상이 나타나는가? 가령 /usr/include/sys/realfile.h라는 파일을 /usr/src/uts/sys/testfile.h과 연결하려고 하고 /usr/include/sys 폴더를 /usr/src/uts/sys 폴더로 연결하려고 한다. 그러면 아래와 같은 구조로 보인다.


이렇게 접근했을 때, /usr/src/uts/sys/ 폴더와 /usr/include/sys 폴더는 무슨 관계인가? 두 개는 같은 파일이다. 같은 파일이라는 뜻은 같은 metadata를 가지고 있다는 뜻이고 그 뜻은 같은 inode table index를 갖는다고 이해하면 된다. 

(OS에서 파일 시스템을 논할 때 "같은 파일"이라는 말을 들으면 이 관계를 머리 속에서 떠올려야 한다.)


이제 다음 이야기를 해보자. 다음에 이야기 할 부분은 mounting이다. 마운팅이란 무엇인가? 우리가 흔히 생각하는 마운팅이란 그냥 어떤 것을 접근가능하도록 파일시스템에 붙이는 거. 이 정도이다. 정확한 정의는 다음과 같다. 하나의 파일 시스템을 다른 파일시스템의 어떤 location에 붙이는 것이다. Windows와 Mac-OS는 Auto mounting이 지원된다. 그래서 Windows에서 USB를 꽂으면 F드라이브 등으로 자동으로 생성되는 것이다. Unix 시스템에서는 일일이 USB 등을 마운트 해줘야 한다. 하나의 파일 시스템을 다른 파일 시스템의 location에 붙인다는 말이 무슨 말일까?


아래 두 개의 파일 시스템이 있다. 



File System2를 File system 1의 dev 에 마운팅 시키면 다음과 같이 된다.



이제 마운팅의 개념은 잘 알겠지요? 한 가지 덧붙여서 이야기 하자면 File System 1의 dev와 같이 다른 파일 시스템이 마운팅 되는 포인트를 Mount Point라고 한다. 이 마운트 포인트는 비어 있는 것이 일반적이다. 


다소 짧은 경향이 없지 않아 있지만 이번 포스팅은 여기서 그만해야 할 것 같다. 다음에 이야기 할 부분이 너무 길다는 것이 그 이유이다. 다음에는 파일 시스템에서 중요하게 논의되어야 할 Consistency를 비롯하여 Protection에 대한 이야기까지 해볼 생각이다.

'Operating Systems' 카테고리의 다른 글

[OS] Memory Management Scheme  (8) 2013.06.30
[OS] File System (Protection)  (1) 2013.05.18
[OS] File System  (0) 2013.05.18
[OS] File System  (4) 2013.05.17
[OS] Deadlock Detection  (2) 2013.02.03
[OS] Deadlock Avoidance (Banker's Algorithm)  (5) 2013.02.03
Posted by 빛나유

댓글을 달아 주세요