brunch

You can make anything
by writing

C.S.Lewis

by 조승혁 Feb 11. 2021

C언어를 위한 넓고 얕은 지식

C언어, 어디까지 알고 있니?

인공지능이 대중들에게 널리 알려지게 되면서, 코딩에 관심을 가지는 사람들이 많아지고 있다. 그리고 필자는 기존에 작성했던 글들의 통계를 보면서 뼈저리게 느끼고 있다. 연관검색어에 '코딩'이란 단어가 들어가는 경우가 부쩍 늘고 있다. 이러한 코딩 열풍 속에서, 많은 사람들이 C언어로 입문을 하고 있다. Python이 강세라는 말이 있지만, 여전히 C언어로 시작하는 사람들이 많다. 많은 사람들이 어려워하기도 하지만, 완전히 정복했다고 생각하는 사람들도 같이 늘고 있다. 하지만 코딩 교육으로 배울 수 있는 C언어의 한계는 극명하다. 왜냐하면 코딩 교육은 문법에 치중되기 때문이다. 물론 진정한 컴퓨터공학도가 되는 게 목표가 아니라면 크게 상관없을 수도 있다. 하지만 C언어는 다르다. C언어는 문법 이외의 지식도 있어야 간단한 프로그램도 구현하는데 지장이 없어진다. 아래 사진이 대표적인 예시다.

아무 문제없이 돌아가는, 문제 있어 보이는 C언어 코드

그만큼 C언어는 어렵고, low-level 언어라고 불리는데 이유가 있다. 이 글에서는 위의 사진 이외에도, 문법만 보면 도저히 이해할 수 없는 경우들을 소개한다. 그리고 각 상황을 설명하는 키워드들을 제시한다. 이들을 잘 따라간다면, 컴퓨터 시스템에 대해서도 공부할 수 있는 기회가 될 것이다. 아직 C언어에 익숙하지 않다면, 익숙해지고 이 글을 보는 것을 추천한다.


반복문의 순서에 따라 프로그램의 성능이 달라진다?

아주 간단한 프로그램을 구현할 때도 반복문(for, while 문)은 빠지지 않는다. 그만큼 코딩을 할 때 자주 사용하고, 중요한 요소이다. 아래는 그러한 반복문을 사용하는 프로그램 중 하나이다. 눈으로만 봐도 어떤 기능을 하는지 알 수 있을 만큼 간단하다.

https://gist.github.com/seunghyukcho/8d979a698a86c265f57bd12d0899d2d2

sumarrayrows와 sumarraycols 함수는 읽어보면 알겠지만, 2차원 배열을 입력으로 받아 모든 원소의 합을 구하는 프로그램이다. 같은 배열에 대해 반환하는 값은 같다. 하지만 한 가지 차이를 보이고 있다. 바로 반복문의 순서이다. 두 함수 모두 (i, j) 번째 원소를 읽고 있다. 근데 i를 훑어보는 반복문과 j를 훑어보는 반복문의 순서가 서로 다르다. 아마 여기서 대부분의 사람들은 '그래서 그게 뭐 어쨌는데?'라고 생각할 것이다. 문법만 본다면 차이가 아예 없기 때문이다. 하지만 시스템 프로그래밍과 관련된 전공책에서는 sumarrayrows 함수가 sumarraycols 함수보다 25배 빠르다고 한다. 그리고 실제로 M과 N을 크게 잡아서 돌려보면, 그 정도 격차가 나는 것을 여러분들도 확인할 수 있을 것이다. 하지만 이를 본다고 해서 자신의 코드를 수정할 필요성을 느끼는 사람들을 적을 것이다. 왜냐하면 많은 사람들이 sumarrayrows처럼 구현해야 한다고 처음에 배우기 때문이다. 이미 row를 기준으로 생각하는 습관이 잡혀있는 것이다(사실 이 특성은 프로그래밍 언어마다 다를 수 있는데, 파스칼의 경우는 sumarraycols 가 훨씬 빠르다). 아무튼 이렇게 차이가 나는 근본적인 이유는 CPU에 있는 캐시(cache)라는 녀석 때문이다. 그래서 sumarrayrows처럼 cache를 염두에 두면서까지 프로그램을 최적화하는 것을 'cache-friendly' 하다고 한다. 자세한 이야기들은 이 글에서 잘 설명해두었으니 궁금하다면 참조해라.


똑같은 연산을 알아서 합쳐준다?

C언어와 같은 컴파일러(compiler)가 있는 프로그래밍 언어들은 다른 언어들과 비교하여 많은 규칙들이 있다. 그리고 규칙들이 많다는 것은, 이들을 기반으로 여러 가지 법칙들을 쉽게 이끌어 낼 수 있다는 것을 의미한다. 그래서 컴파일러에서 기본적으로 제공하는 최적화 방식들이 많이 존재한다. 아래의 코드도 그중 하나이다.

https://gist.github.com/seunghyukcho/b338295dfb8bdb1550386cb79d62b626

원래 코드를 보면 b * c 가 두 번 반복하는 것을 볼 수 있다. 그리고 컴파일러는 이를 하나의 변수로 만들어, 한 번만 연산을 하도록 만든다. 즉, 동일한 부분을 한 번만 연산하도록 바꾼 것이라고 이해할 수 있다. python과 같은 interpreter 형식의 언어들은 한 줄씩 기계어로 번역하기 때문에 이러한 최적화가 불가능하다. C언어는 컴파일러를 보유하고 있기 때문에 이러한 최적화가 가능한 것이다. 그래서 산술 연산 차원에서의 복사 - 붙여 넣기는 너무 걱정하지 않아도 된다. 어차피 컴파일러가 알아서 하나로 합쳐주기 때문이다(물론 가독성은 떨어지겠지만). 이러한 최적화 방식을 다른 말로 common subexpressions이라고 한다.


복잡한 연산을 간단한 연산으로 알아서 바꿔준다?

common subexpressions 이외에도 다양한 컴파일러에서 해주는 최적화들이 있다. 그중에서 직관적인 방법들을 골라, 코드로 구현해봤다. 아래 코드들을 한 번 보자.

https://gist.github.com/seunghyukcho/79ef2efdf7124f635d4bf4208922a743

위의 코드는 x * x를 반복문 바깥으로 뺀 것이다. 이유는 x라는 변수 자체가 반복문의 index 인 i와 관계가 없기 때문에, 번 연산할 이유가 없기 때문이다. 이는 코드 일부분을 이동시킨다고 하여, code motion이라고 부른다. 주로 반복문의 index와 연관 지어 최적화하기 때문에 'loop-invariant code motion' 이라고도 부른다. 아래 코드의 경우는 곱셈(*) 연산을 덧셈(+)으로 바꿔준 것이다. 이는 인간의 입장에서 보면 의미 없는 행위일 수 있다. 하지만 컴퓨터의 관점에서는 곱셈이 덧셈보다 훨씬 느리고 복잡하다. 그래서 곱셈을 더 단순한 연산인 덧셈으로 치환한 것이다. 이처럼 복잡한 연산을 간단한 연산으로 바꾸는 최적화를 strength reduction이라고 부른다.


이처럼 컴파일러라는 존재 자체가 같은 코드를 구현해도, 훨씬 성능이 좋은 프로그램으로 둔갑시킬 가능성을 내포하고 있다. 하지만 python과 같은 interpreter 언어들이 여전히 인기를 얻는 이유는, 역시 성능보다 사용성을 추구하는 사용자들이 많기 때문일 것이다.


이름은 같은데, 자료형이 다른 변수를 선언할 수 있다?

이번에 소개할 C언어의 특성은, 프로그램이 여러 개의 파일로 이루어졌을 때 나타난다.

https://gist.github.com/seunghyukcho/6e103cd2c9e0f47199568b61ad2e1902

a.c를 보면 xy를 특정 값으로 초기화하고, f() 함수를 실행하여 x의 값을 바꾼다. 그리고 f()를 실행하기 이전과 이후에 x, y의 값을 출력하여 확인한다. 일단 실행 결과를 예측하기 이전에, 이상한 점이 하나 있는 것을 알 수 있다. 바로 x가 다른 자료형으로 두 번 정의되었다는 것이다. 컴파일 에러가 나야 할 것 같지만, 그러한 일 없이 실행파일이 잘 만들어진다. 이와 관련된 정보들은 여기를 보면 알 수 있다. 아무튼 두 번 정의할 수 있다 하더라도 결국 x의 값만 바뀌어야 한다. 하지만 아래 실행결과를 보면, y의 값도 바뀐 것을 알 수 있다.

위 코드를 컴파일하여 실행한 결과, x를 수정했는데 y도 같이 수정된 모습

이렇게 된 이유를 요약하면 1) xy가 메모리 상에서 붙어있다 2) double은 int의 2배이다. 이해가 안 된다면 그냥 넘어가도 된다. 이 예시를 설명한 이유는, 전역 변수를 쓰는 것에 대해 경각심을 느끼라는 것이다. 위와 같은 사례는 작은 프로젝트에서는 문제가 안되지만, 파일 수가 많아지면 엄청난 여파를 불러일으킬 수 있다. 그리고 이는 링커(linker)와 컴파일러가 잡아주지 못하기 때문에, 더 찾기 힘들 것이다. 그렇기 때문에 대규모 프로젝트를 C언어로 하게 된다면, 꼭 전역 변수 사용에 주의하기 바란다.


번외) 링커에 넣어주는 파일들의 순서

링커는 여러 파일들을 하나의 프로그램으로 합쳐주는 역할을 수행한다. 예전에는 이 링커에 입력으로 주는 파일의 순서를 잘못 주면 오류가 발생한다고 알려졌었다. 실제로 검색해보면 이와 같은 자료들이 많이 나온다(자료 1, 자료 2). 하지만 자료들에서 주는 예시들을 돌려보니, 잘 돌아갔다. 그래서 C언어의 컴파일러 중 하나인 gcc의 공식 자료를 보니, 다음과 같은 글귀가 있었다.

The command-line options to ld may be specified in any order, and may be repeated at will.



이처럼 C언어는 low-level 프로그래밍 언어인 만큼, 같은 기능을 하는 코드여도 그 구현 방식에 따른 성능 차이가 많이 난다. 그렇기에 C언어를 쓰려면 위와 같은 특성들 이외에 공부를 많이 해야 한다. 만약 C언어를 더 깊게 파고 싶은 사람들은 "Computer Systems A Programmers Perspective"를 추천한다. 번역서도 있으니, 번역서를 사는 것을 추천한다. 필자가 봤던 전공서적들 중에서 거의 유일하게 번역이 잘된 책이라고 생각한다.

작가의 이전글 RoBERTa
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari