« Previous : 1 : 2 : 3 : 4 : 5 : 6 : 7 : Next »

1. C/C++이 빌드가 느린 이유

베테랑 프로그래머라면 이미 다 알기도 하겠지만, C/C++ (특히 C++)은 강력한 대신 정말 만년 굼벵이 언어가 될 수밖에 없는 요인만 일부러 골고루 가진 채 만들어졌다 해도 과언이 아닌 것 같다.

뭐가 굼벵이냐 하면 두 말할 나위도 없이 빌드 속도 말이다. C#, 자바, 델파이 같은 다른 언어나 툴과 비교하면 안습 그 자체. 이건 컴퓨터의 속도만 빨라진다고 해서 극복 가능한 차원의 차이가 아니라 구조적으로 심한 부담과 비효율 때문이다. 이 점에 대해서는 본인도 예전에 여러 글을 블로그에다 언급한 적이 있다.

  • 일단 C++은 태생이 바이트코드 같은 가벼운 가상 기계가 아니라 철저하게 기계어 네이티브 코드 생성 지향이다. 다른 가벼운(?) 언어들과는 위상부터가 다르며, 이 상태에서 최적화까지 가면 부담은 더욱 커진다. 게다가 이 언어는 설계 철학이 컴파일 시점 때 최대한 많은 걸 미리 결정하는 걸 지향하고 있다. 가령, 자바에 inline이라든가 함수 호출 규약, 레지스터, C++ 수준의 static한 템플릿 메타프로그래밍, 혹은 링크 타임 코드 생성 같은 개념이 있지는 않다.
  • 또한 이 언어는 근본적으로 문법이 상당히 문맥 의존적이고 복잡하여, 구문 분석이 어렵다. 단적인 예로 함수 선언과 객체 선언 A b(c); 변수 선언과 단순 연산식 B*c; 형변환 연산과 단순 연산식 (c)+A 가 c가 무엇인지 문맥에 따라 왔다갔다 하면서 완전히 달라진다. 거기에다 C++의 경우 템플릿, 오버로딩, namespace ADL까지 가면 난이도는 정말 안드로메다로. 다른 언어는 O(n log n) 시간 복잡도만으로도 되는 구문 분석 작업이 C++은 반드시 O(n^2)을 쓰지 않으면 안 되는 과정이 있다고 한다.
  • 빌드를 위해 전처리, 링크 같은 복잡한 계층이 존재하며, 특히 링크는 병렬화도 되지 않아 속도를 더욱 올릴 수가 없는 작업이다. 한 모듈에서 참조하는 함수의 몸체가 다른 어느 번역 단위에 있을지는 전혀 알 수 없다!
    그런데 요즘 C++ 컴파일러의 트렌드는 1에서 잠시 언급했듯이 링크 타임 때의 코드 생성과 최적화(인라이닝 포함)여서 이런 병목 지점에서 더욱 많은 작업량이 부과되고 있다. 이런??

이런 특징은 유독 C/C++ 언어만 개발툴/IDE에서 프로젝트를 만들면 온갖 잡다한 보조 데이터 파일들이 많이 생성되는 이유와도 일맥상통한다. 소스 코드를 잽싸게 분석하여 인텔리센스 같은 똑똑한 IDE 기능을 제공하기가 여타 언어들보다 훨씬 더 어렵기 때문이다.

2. 인클루드의 문제점

그런데, 네이티브 코드 생성, 복잡한 문법 같은 것 이상으로 C/C++의 빌드 시간을 더욱 뻥튀기시키고 빌드 작업을 고달프게 하는 근본적인 요소는 전처리 중에서도 다름아닌 #include 남발이다. C/C++은 남이 만들어 놓은 함수, 클래스, 구조체 같은 프로그래밍 요소를 쓰려면 해당 헤더 파일을 무조건 인클루드해 줘야 한다.

일단 이건 문법적으로는 인위적인 요소가 없이 깔끔해서 좋다. 인클루드되는 헤더는 역시 C/C++ 문법대로 작성된 일반 텍스트 파일이며, 내가 짜는 프로그램이 참조하는 명칭들의 출처가 여기 어딘가에는 반드시 있다고 보장됨을 내 눈으로 확인할 수 있다. 그러나 DB 형태로 최적화된 바이너리 파일이 아니라 파싱이 필요한 텍스트 파일이란 점은 일단 빌드 속도의 저하로 이어진다. 이게 문제점 하나.

본격적인 C++ 프로그램을 하나 만들려면 표준 C/C++ 라이브러리뿐만이 아니라 윈도우 API, MFC, 그리고 다른 3rd-party 라이브러리, 게임 엔진 등 갖가지 라이브러리나 프레임워크가 제공하는 헤더 파일을 참조하게 된다. 이것들을 합하면 한 소스 코드를 컴파일하기 위해 인클루드되는 헤더 파일은 가히 수십, 수백만 줄에 달하게 된다.

게다가 이 인클루드질은 전체 빌드를 통틀어 한 번만 하고 끝인 게 아니라, 이론적으로는 매 번역 단위마다 일일이 새로 해 줘야 한다. 헤더 파일 의존도가 개판이 돼 버리는 바람에 헤더 파일 하나 고칠 때마다 수백 개의 cpp 파일이 재컴파일되는 문제는 차라리 애교 수준이다. 문제점 둘.

보통 헤더 파일에는 중복 인클루드 방지를 위한 guard가 있다.

#ifndef ___HEADER_DEFINED_
#define ___HEADER_DEFINED_

/////

#endif

그런데 #if문 조건을 만족하지 못하는 줄들은 단순히 구문 분석과 파싱만 skip될 뿐이지, 컴파일러는 여전히 중복 인클루드된 헤더 파일도 각 줄을 일일이 읽어서 #else나 #endif가 나올 때까지 들여다보긴 해야 한다.

많은 사람들이 간과하는 사실인데(사실 나도 그랬고), 그때는 컴파일 작업만 잠시 중단됐을 뿐, 전처리기는 전체 소스를 대상으로 여전히 동작하고 있다. 중복 인클루드가 컴파일러의 파일 액세스 트래픽을 얼마나 증가시킬지가 상상이 되는가? guard만 있다고 장땡이 아니며, 이게 근본적으로 얼마나 비효율적인 구조인지를 먼저 알아야 한다. 문제점 셋.

그리고 이 #include의 수행 결과를 컴파일러나 IDE로 하여금 예측이나 최적화를 도무지 할 수가 없게 만드는 치명적인 문제는 극단적인 문맥 의존성이다.
헤더 파일은 그저 static한 함수, 클래스, 변수 선언의 집합체가 아니다. 엄연히 C/C++ 소스 코드의 일부를 구성하며, 동일한 헤더라 해도 어떤 #define 심벌이 정의된 상태에서 인클루드하느냐에 따라서 그 여파가 완전히 달라질  수 있다.

극단적인 예로, 한 소스 파일에서 #define 값만 달리하면서 동일 헤더 파일을 여러 번 인클루드함으로써, 템플릿 비스무리한 걸 만들 수도 있단 말이다. 일례로, 비주얼 C++ 2010의 CRT 소스에서 strcpy_s.c와 wcscpy_s.c를 살펴보기 바란다. 베이스 타입만 #define을 통해 char이나 wchar_t로 달리하여 똑같이 tcscpy_s.inl을 인클루드하는 걸로 구현돼 있음을 알 수 있다...;;

물론 인클루드를 실제로 그렇게 변태적으로 활용하는 예는 극소수이겠지만 인클루드는 여타 언어에 있는 비슷한 기능인 import나 use 따위와는 차원이 다른 개념이며, 캐싱을 못 하고 그 문맥에서 매번 일일이 파싱해서 확인해 보는 수밖에 없다. 문제점 넷이다.

이런 문제 때문에 여타 언어들은 텍스트 파싱을 수반하는 인클루드 대신, 별도의 패키지 import 방식을 쓰고 있으며, Objective C도 #include 대신 #import를 제공하고 헤더 파일은 무조건 중복 인클루드가 되지 않는 구조를 채택하여 셋째와 넷째 문제를 피해 갔다.

비주얼 C++도 #pragma once라 하여 #endif를 찾을 것도 없이 중복 인클루드를 방지하고 파일 읽기를 거기서 딱 중단하는 지시자를 추가했다. 이건 비표준 지시자이긴 하지만 전통적인 #ifdef~#endif guard보다 빌드 속도를 향상시키는 테크닉이기 때문에 프로그래머의 입장에서는 사용이 선택이 아닌 필수이다. 물론, 단순히 중복 선언 에러만 방지하는 게 아니라 특정 헤더 파일의 인클루드 여부를 알려면 재래식 #define도 좀 해 줘야겠지만 말이다.

외부에서 기선언된(predefined) 프로그래밍 요소를 끌어오는데, namespace나 package 같은 언어 계층을 거친 명칭이 아니라 생(raw-_-) 파일명의 지정이 필요한 것부터가 오늘날의 관점에서는 꽤 원시적인 작업이다. 개인적으로는 인클루드 파일의 경로를 찾는 메커니즘도 C/C++은 너무 복잡하다고 생각한다.

""로 싸느냐 <>로 싸느냐부터 시작해서, 인클루드가 또 다른 파일을 중첩으로 인클루드할 때, 다른 인클루드 파일을 자기 디렉터리를 기준으로 찾을지 자신을 인클루드 한 부모 파일의 위치로부터 찾을지, 프로젝트 설정에 명시된 경로에서 찾을지 같은 것 말이다…;; 게다가 인클루드 명칭도 #define에 의한 치환까지 가능하다. #include MY_HEADER처럼. 그게 가능하다는 걸 FreeType 라이브러리의 소스를 보면서 처음으로 알았다.

그런데 그러다가 서로 다른 디렉터리에 있는 동명이인 인클루드 파일이 잘못 인클루드되기라도 했다면.. 더 이상의 자세한 설명은 생략. 내가 무심코 선언한 명칭이 어디엔가 #define 매크로로도 지정되어 있어서 엉뚱하게 자꾸 치환되고 컴파일 에러가 나는 것과 같은 악몽이 발생하게 된다! 문제점 다섯.

이것도 어찌 보면 굉장히 문맥 의존적인 절차이기 때문에, 오죽했으면 비주얼 C++ 2010부터는 인클루드/라이브러리 디렉터리 지정을 global 단위로 하는 게 완전히 없어지고 전적으로 프로젝트 단위로만 하게 바뀌었다는 걸 생각해 보자.

C++ 프로젝트에서 MFC의 클래스나 윈도우 API의 함수를 찍고 '선언으로 가기'를 선택하면 afxwin.h라든가 winbase.h 같은 표준 인클루드 파일에 있는 실제 선언 지점이 나온다. 그 방대한 헤더 파일을 매 빌드 때마다 일일이 파싱할 수가 없으니 인텔리센스 DB 파일 같은 건 정말 크고 아름다워진다.

그에 반해 C# 닷넷 프로젝트에서 Form 같은 클래스의 선언을 보면, 컴파일러가 바이너리 수준에서 내장하고 있는 클래스의 껍데기 정보가 소스 코드의 형태로 생성되어 임시 파일로 뜬다…;; 이게 구시대 언어와 신세대 언어의 시스템 인프라의 차이가 아닌가 하는 생각이 들었다.

그래서 이런 C++ 인클루드 체계의 비효율 문제는 어제오늘 제기되어 온 게 아니기 때문에, 컴파일러 제조사도 좀 더 근본적인 문제 회피책을 간구하게 됐다. 그래서 나온 것이 그 이름도 유명한 precompiled 헤더이다. stdio.h나 stdlib.h 정도라면 모를까, 매 번역 단위마다 windows.h나 afx.h를 일일이 인클루드해서 파싱한다는 건 삽질도 그런 삽질도 없으니 말이다..

3. precompiled header의 도입

일단 프로젝트 내에서 "인클루드 전용" 헤더 파일과 이에 해당하는 번역 단위를 설정한다. 비주얼 C++에서 디폴트로 주는 명칭이 바로 stdafx.cpp와 stdafx.h이다. 모든 번역 단위들이 공용으로 사용하는 방대한 양의 프레임워크, 라이브러리의 헤더를 몰아서 인클루드시킨다. 컴파일러 옵션으로는 Create precompiled header에 해당하는 /Yc "stdafx.h"이다.

그러면 그 헤더 뭉치들은 stdafx.cpp를 컴파일할 때 딱 한 번만 실제로 인클루드와 파싱을 거치며, 이 파일들의 분석 결과물은 빠르게 접근 가능한 바이너리 DB 형태인 프로젝트명.pch 형태로 생성된다.

그 뒤 나머지 모든 소스 파일들은 첫 줄에 #include "stdafx.h"를 반드시 해 준 뒤, Use precompiled header인 /Yu "stdafx.h" 옵션으로 컴파일한다. 그러면 이제 stdafx.h의 인클루드는 실제 이 파일을 열어서 파싱하는 게 아니라, 미리 만들어진 PCH 파일의 심벌을 참고하는 것으로 대체된다! 앞에서 제기한 인클루드의 문제점 중 첫째와 둘째를 극복하는 셈이다.

pch 파일이 생성되던 시점의 문맥과 이 파일이 실제로 인클루드되는 시점의 문맥은 싱크가 서로 반드시 맞아야 한다. 그렇기 때문에 소스 코드에도 문맥상의 제약이 걸린다. PCH를 사용한다고 지정해 놓고 실제로 stdafx.h를 맨 먼저 인클루드하지 않은 소스 파일은 Unexpected end of file이라는 컴파일 에러가 발생하게 된다. PCH 개념을 모르는 프로그래머는 C++ 문법에 아무 하자가 없는 외부 소스 코드가 왜 컴파일되지 않는지 이해를 못 할 것이다.

당연한 말이지만 stdafx.h가 인클루드하는 헤더 파일의 내용이 수정되었다면 PCH 파일은 다시 만들어져야 하며, 이때는 사실상 프로젝트 전체가 리빌드된다. 그러므로 stdafx.h 안에는 거의 수정되지 않는 사실상 read-only인 헤더 파일만 들어가야 한다.

인클루드 파일만 수십, 수백만 줄에 달하는 중· 대형 C++ 프로젝트에서 PCH가 없다는 건 상상조차 할 수 없는 일이다. 얼마 되지도 않는(많게 잡아도 200KB 이내) 소스 코드들이 high-end급 컴에서 그것도 네트워크도 아닌 로컬 환경에서 빌드 중인데 소스 파일 하나당 컴파일에 1초 이상씩 잡아 처먹는다면, 이건 인클루드 삽질 때문일 가능성이 매우 높다. 이 경우, 당장 PCH를 사용하도록 프로젝트 설정을 바꾸고 소스 코드를 리팩터링할 것을 심각하게 고려해야 한다. 이건 작업 생산성과 직결된 문제이기 때문이다.

아놔, 이렇게 글을 길게 쓸 생각은 없었는데 너무 길어졌다.
요컨대 C++ 프로그래머라면 자기의 생업 수단인 언어가 이런 구조적인 비효율을 갖고 있다는 걸 인지하고, 상업용 컴파일러 및 개발툴이 이를 극복하기 위해 어떤 대안을 내놓았는지에 대해 관심을 가질 필요가 있다.

자바, C#, D 등 C++의 후대 언어들은 C++과 문법은 비슷할지언정 이 인클루드 체계만은 어떤 형태로든 제각각 다 손을 보고 개량했음을 알 수 있다. 아까도 언급했듯, 하다못해 Objective C도 중복 인클루드 하나만이라도 자기 식으로 정책을 바꿨지 않던가.

한 가지 생각할 점은, C/C++은 태생이 이식성에 목숨을 걸었고, 언어의 구현을 위해 바이너리 레벨에서 뭔가 이래라 저래라 명시하는 것을 극도로 꺼리는 언어라는 점이다. 그래서 대표적으로 C++ 함수 decoration이 알고리즘이 중구난방인 아주 대표적인 영역이며, 함수 calling convension도 여러 규격이 난립해 있고 모듈/패키지 같은 건 존재하지도 않는다. 그런 차원에서, 비록 비효율적이지만 제일 뒤끝 없는 텍스트 #include가 여전히 선호되어 온 건지도 모르겠다.

4. 여타 언어의 인클루드

여담이다만, 본인은 베이직부터 쓰다가 C/C++로 갈아탄 케이스이기 때문에 인클루드라는 걸 처음으로 접한 건 C/C++이 아니라 퀵베이직을 통해서였다.

'$INCLUDE: 'QB.BI'

바로, 도스 API를 호출하는 인터럽트 함수와 관련 구조체가 그 이름도 유명한 저 헤더 파일에 있었기 때문이다.

C/C++에 전처리기가 있는 반면, 베이직이나 파스칼 계열 언어는 개념적으로 그런 전처리기와 비슷한 위상인 조건부 컴파일이나 컴파일 지시자는 주석 안에 메타커맨드의 형태로 들어있곤 했다. 그러나 여타 프로그래밍 요소를 끌어다 오는 명령은 메타커맨드나 전처리기가 아니라, 엄연히 언어 예약어로 제공하는 게 디자인상 더 바람직할 것이다.

그리고 파워베이직은 퀵베이직 스타일의 인클루드 메타커맨드도 있고, 파스칼 스타일의 패키지 지정 명령도 둘 다 갖추고 있었다.

Posted by 사무엘

2012/09/21 19:25 2012/09/21 19:25
,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/735

C/C++, 자바, C# 비교

전산학의 초창기이던 1950년대 후반엔 프로그래밍 언어의 조상이라 할 수 있는 코볼, 포트란 같은 언어가 고안되었다. 그리고 이때 범용적인 계산 로직의 기술에 비중을 둔 알골(1958)이라는 프로그래밍 언어가 유럽에서 만들어졌는데, 이걸 토대로 훗날 파스칼, C, Ada 등 다양한 언어들이 파생되어 나왔다.

이때가 얼마나 옛날이냐 하면, 셸 정렬(1959), 퀵 정렬(1960) 알고리즘이 학술지를 통해 갓 소개되던 시절이다. 구현체는 당연히 어셈블리어.;; 그리고 알골이 도입한 재귀호출이라는 게 함수형 언어가 아닌 절차형 언어에서는 상당히 참신한 개념으로 간주되고 있었다. 전산학의 역사를 아는 사람이라면, 컴퓨터를 돌리기 위해 프로그래밍 언어가 따로 만들어진 게 아니라, 프로그래밍 언어를 구현하기 위해 컴퓨터가 발명되었다는 걸 알 것이다.

알골 자체는 시대에 비해 언어 스펙이 너무 복잡하고 막연하기까지 하며, 구현체를 만들기가 어려워서 IT 업계에서 실용적으로 쓰이지 못했다. 그러나 후대의 프로그래밍 언어들은 알골의 영향을 상당히 많이 받았으니 알골은 가히 프로그래밍 언어계의 라틴어 같은 존재로 등극했다.

물론, 그로부터 더 시간이 흐른 오늘날은 알골의 후예에 속하는 언어인 C만 해도 이미 라틴어 같은 전설적인 경지이다. 중괄호 블록이라든가 C 스타일의 연산자 표기 같은 관행은 굳이 C++, 자바, C# 급의 언어 말고도, 자바스크립트나 PHP처럼 타입이 엄격하지 않고 로컬이 아닌 웹 지향 언어에도 그런 관행이 존재하니 말이다.

C가 먼저 나온 뒤에 거기에 OOP 속성이 가미되어 C++이라는 명작/괴작 언어가 탄생했다. C가 구조화 프로그래밍을 지원하는 고급 언어에다가 어셈블리어 같은 저급 요소를 잘 절충했다면, C++은 순수 OOP 개념의 구현보다는 역시 OOP 이념을 C 특유의 성능 지향 특성에다가 적당히 절충을 잘 했다. 그래서 C++이 크게 성공할 수 있었다.

잘 알다시피 C/C++은 모듈이나 빌드 구조가 컴파일 지향적이며, 거기에다 링크라는 추가적인 작업을 거쳐서 네이티브(기계어) 실행 파일을 만드는 것에 아주 특화되어 있다.

번역 단위(translation unit)라고 불리는 개개의 소스들은 프로그래밍에 필요한 모든 명칭들을 텍스트 형태의 다른 헤더 파일로부터 매번 include하여 참조한 뒤, 컴파일되어 obj 파일로 바뀐다.
한 번역 단위에서 참조하는 외부 함수의 실제 몸체는 어느 번역 단위에 있을지 알 수 없다. 어차피 링크 때 링커가 모든 obj 파일들을 일일이 뒤지면서 말 그대로 연결을 하게 된다.

이 링크를 통해 드디어 실행 파일이나 라이브러리 파일이 최종적으로 만들어진다. 실행 파일은 대상 운영체제가 인식하는 실행 파일 포맷을 따라 만들어지지만 static 라이브러리는 그저 obj들의 모음집일 뿐이기 때문에 lib 파일과 obj 파일은 완전히 같지는 않아도 내부 구조가 크게 차이가 나지 않는다.

이런 일련의 컴파일-링크 계층이 C/C++을 로컬 환경에서의 매우 강력한 고성능 언어로 만들어 주는 면모가 분명 있다. 또한 197, 80년대에는 컴퓨터 자원의 한계 때문에 원천적으로 언어를 그런 식으로 설계해야 하기도 했다.
그러나 오늘날은 대형 프로젝트를 진행할 때 C/C++의 그런 디자인은 심각한 비효율을 초래하기도 한다. 내가 늘 지적하듯이 C보다도 특히 C++은 안습할 정도로 빌드가 너무 느리고 생산성이 떨어진다.

그에 반해 자바는 문법만 살짝 비슷할 뿐 디자인 철학은 C++과는 완전히 다른 언어이다. 잘 알다시피 자바는 의도하는 동작 환경 자체가 native 기계어가 아니라 플랫폼 독립적인 자바 가상 기계이다. 컴퓨터 환경이 발달하고 웹 프로그래밍이 차지하는 비중이 커진 덕분에 이런 발상이 나올 수가 있었던 셈이다.

모든 자바 프로그램은 무조건 1코드, 1클래스이며(단, 클래스 내부에 또 다른 클래스들이 여럿 있을 수는 물론 있음), 심지어 소스 파일 이름과 클래스 이름이 반드시 일치해야 한다. 클래스가 곧 C/C++의 ‘번역 단위’와 강제로 대응한다. 그리고 컴파일된 자바 소스는 곧장 컴파일된 바이트코드로 바뀌며, 이것이 자바 VM이 있는 곳이라면 어디서나 돌아가는 실행 파일(EXE)도 되고 라이브러리(DLL, OBJ)도 된다. 물론, 여러 라이브러리들의 집합체인 JAR이라는 포맷도 따로 있기도 하고 말이다.

클래스 내부에 public static void main 메소드(멤버 함수)만 구현되어 있으면 곧장 실행 가능하다. C++은 C와의 호환을 위해 시작 함수가 클래스 없는 일반 main으로 동일하게 지정돼 있는 반면, 자바는 global scope이 존재하지 않고 모든 명칭이 클래스에 반드시 소속돼 있어야 하기 때문에 그렇다. javac 명령으로 소스 코드(*.java)를 컴파일한 뒤, java 명령으로 컴파일된 바이트코드(*.class)를 실행하면 된다.

다른 모듈을 끌어다 쓸 때도 import로 바이너리 파일을 곧장 지정하면 되니, 텍스트 파싱이 필요한 C++의 #include보다 효율적이다. 번거롭게 *.h와 *.lib (그리고 심지어 *.dll까지)를 일일이 따로 구비할 필요 없다.

요컨대 자바는 C++에 비해 굉장히 많은 자유도와 성능을 제약한 대신, C++보다 훨씬 더 손이 덜 가도 되고 빌드도 훨씬 빨리 되고 프로젝트 세팅도 월등히 더 간편하게 되게 만들어졌다. 함수 호출 규약, 인라이닝 방식, C++ symbol decoration, 링크 에러, CRT의 링크 방식, link-time 코드 생성 최적화 같은 온갖 골치 아프고 복잡한 개념들이 자바에는 전혀 존재하지 않는다.
C++이 벙커에 시즈 탱크에 터렛과 마인 등, 손이 많이 가는 테란이라면, 자바는 프로토스 정도는 되는 것 같다.

자바는 하위 호환성을 고려하지 않은 새로운 언어를 만든 덕분에 디자인상 깔끔한 것도 있지만, 상상도 못 할 편리함을 실현하기 위해 성능도 C++ 사고방식으로는 상상도 못 할 정도로 많이 희생한 것 역시 사실이다. 이는 단순히 메모리 garbage collector가 존재하는 오버헤드 이상이다.

그래서 요즘은 자바 바이트코드를 언어 VM이 그때 그때 실시간으로 네이티브 코드로 재컴파일하여, 자바로도 조금이라도 더 빠른 속도를 내게 하는 JIT(just in time)기술이 개발되어 있다. 비록 이 역시 한계가 있을 수밖에 없겠지만 한편으로는 구조적으로 유리한 점도 있다.

컴파일 때 모든 것이 결정되어 버리는 C++ 기반 EXE/DLL은 사용자의 다양한 실행 환경을 예측할 수 없으니 보수적인 기준으로 빌드되어야 한다. 그러나 자바 프로그램의 경우, VM만 그때 그때 최신으로 업데이트하여 최신 CPU의 명령이나 병렬화 테크닉을 쓰게 하면 그 혜택을 모든 자바 프로그램이 자동으로 보게 된다. 물론 C++로 치면 cout이 C의 printf보다 코드 크기가 작아지는 경지에 다다를 정도로 컴파일러가 똑똑해져야겠지만 말이다.

자바 얘기가 길어졌는데, 다음으로 C#에 대해서 좀 살펴보기로 하자.
C# 역시 네이티브 코드 지향이 아니라 닷넷 프레임워크에서 돌아가는 바이트코드 기반인 점, 복잡한 링크 메커니즘을 생략하고 C++의 지나치게 복잡한 문법과 모듈 구조를 간소화시켰다는 점에서는 자바와 문제 접근 방식이 같다.

단적인 예로, 클래스를 선언하면서 멤버 함수까지 클래스 내부에다 정의를 반드시 집어넣게 한 것, 그리고 생성자 함수의 호출이 수반되는 개체의 생성은 반드시 new를 통해서만 가능하게 한 것은 컴파일러와 링커가 동작하기 상당히 편하게 만든 조치이다. 이는 자바와 C#에 공통적으로 적용된다.

다만 C#은 자바처럼 엄격한 1소스 1클래스 체계는 아니며, 빌드 결과물로 엄연히 일반적인(=윈도우 운영체제가 사용하는 PE 포맷 기반인) EXE와 DLL이 생성된다. 물론 내부엔 기계어 코드가 아닌 바이트코드가 들어있지만 말이다.

C# 역시 클래스 내부에 존재하는 static void Main가 EXE의 진입점(entry point)이 된다. 그러나 C#은 자바 같은 1소스, 1클래스, 1모듈 구조가 아니기 때문에 여러 클래스에 동일한 static void Main이 존재하면 컴파일러가 어느 것을 진입점으로 지정해야 할지 판단할 수 없어서 컴파일 에러를 일으킨다. 링크나 런타임 에러가 아님. 진입점을 별도의 컴파일러 옵션으로 따로 지정해 주거나, Main 함수를 하나만 남겨야 한다.

여담이지만, C#의 진입점 함수는 자바와는 달리 첫 글자 M이 대문자이다. 전통적으로 자바는 첫 글자를 소문자로 써서 setValue 같은 식으로 메소드 이름을 지어 온 반면, 윈도우 세계는 그렇지 않기 때문이다(SetValue).
그리고 C#의 Main은 굳이 public 속성이 아니어도 된다. 어차피 진입점인데 접근 권한이 무엇이면 어떻냐는 식의 발상인 것 같다.

닷넷 실행 파일이 사용하는 바이트코드는 자바와 마찬가지로 기계 독립적인 구조이다. 그러나 그것의 컨테이너라 할 수 있는 윈도우 운영체제의 실행 파일 포맷(PE)은 여전히 CPU의 종류를 명시하는 필드가 존재한다. 그리고 32비트와 64비트에서 필드의 크기가 달라지는 것도 있다. 이것은 기계 독립성을 추구하는 닷넷의 이념과는 어울리지 않는 구조이다. 그렇다면 닷넷은 이런 상황을 어떻게 대처하고 있을까?

내가 테스트를 해 본 바로는 플랫폼을 ‘Any CPU’라고 지정하면, 해당 C# 프로그램은 명목상 그냥 가장 무난하고 만만한 x86 껍데기로 빌드되는 듯하다.
작정하고 x64 플랫폼을 지정하고 빌드하면 헤더에 x64 CPU가 명시된다. 뒤에 이어지는 바이트코드는 어느 CPU에서나 동일하게 생성됨에도 불구하고, 그 프로그램은 x86에서는 실행이 거부되고 돌아가지 않게 된다.

그러니, 64비트 네이티브 DLL의 코드와 연동해서 개발되는 프로그램이기라도 하지 않은 이상, C# 프로그램을 굳이 x64용으로 제한해서 개발할 필요는 없을 것이다. 다만, x86용 닷넷 바이너리는 관례적으로 닷넷 런타임인 mscoree.dll에 대한 의존도가 추가되는 반면 x64용 닷넷 바이너리는 그런 게 붙어 있지 않다. 내 짧은 생각으론, 64비트 바이너리는 32비트에서 호환성 차원에서 넣어 줘야 했던 잉여 사항을 생략한 게 아닌가 싶다.

DLL에 기계 종류와 무관한 리소스나 데이터가 들어가는 일은 옛날부터 있어 왔지만, 닷넷은 코드조차도 기계 종류와 무관한 독립된 녀석이 들어가는 걸 가능하게 했으니 이건 참 큰 변화가 아닐 수 없다. 네이티브 쪽과는 달리 골치 아프게 32비트와 64비트를 일일이 신경 쓸 필요가 없고, 한 코드만으로 x86(-64) 계열과 ARM까지 다 커버가 가능하다면, 정말 어지간히 하드코어한 분야가 아니라면, 월등한 생산성까지 갖추고 있는 C#/자바 같은 개발 환경이 뜨지 않을 수 없을 것 같다. C++과 자바, C#을 차례로 비교해 보니 그런 생각이 들었다.

Posted by 사무엘

2012/06/16 19:37 2012/06/16 19:37
, , ,
Response
No Trackback , 7 Comments
RSS :
http://moogi.new21.org/tc/rss/response/696

C++11의 람다 함수

프로그래밍을 하다 보면, 어떤 컨테이너 자료구조의 내부에 있는 모든 원소들을 순회하면서 각 원소에 대해 뭔가 동일한 처리를 하고 싶은 때가 빈번히 발생한다.
그 절차를 추상화하기 위해 C++ 라이브러리에는 algorithm이라는 헤더에 for_each라는 템플릿 함수가 있다. 다음은 이 함수의 로직을 나타낸 C++ 코드이다. 딱히 로직이라 할 것도 없이 아주 직관적이고 간단하다.

template<typename T, typename F>
void For_Each_Counterfeit(T a, T b, F& c)
{
    for(T i=a; i!=b; ++i) c(*i);
}

C++은 템플릿과 연산자 오버로딩을 통해 자료구조에 대한 상당 수준의 추상화를 달성했다.
iterator에 해당하는 a와 b는 그렇다 치더라도, 여기서 핵심은 c이다.
F가 무엇인지는 모르겠지만, 어쨌든 c는 함수 호출 연산자 ()를 적용할 수가 있는 대상이어야 한다.
그럼 무엇이 가능할까? 여러 후보들이 있다.

일단 C언어라면 함수 포인터가 떠오를 것이다. 함수 포인터는 코드를 추상화하는 데 지금까지 고전적으로 쓰여 온 기법이다.

void foo(char p);

char t[]="Hello, world!";
For_Each_Counterfeit(t, t+strlen(t), foo);

C++에서는 클래스가 존재하는 덕분에 더 다양한 카드가 생겼다. 클래스가 자체적으로 함수 호출을 흉내 내는 연산자를 갖출 수 있기 때문이다.

class MyObject {
public:
    void operator()(char x);
};

For_Each_Counterfeit(t, t+strlen(t), MyObject());

그리고 더욱 기괴한 경우이지만, 클래스 자신이 함수의 포인터로 형변환이 가능해도 된다.

class MyObject {
public:
    typedef void (*FUNC)(char);
    operator FUNC();
};

For_Each_Counterfeit(t, t+strlen(t), MyObject());

C++ 라이브러리에는 functor 등 다양한 개념들이 존재하지만, 그 밑바닥은 결국은 C++ 언어의 이런 특성들을 사용해서 구현되어 있다.
여기서 재미있는 점이 있다. 다른 자료형과는 달리 함수 포인터로 형변환하는 연산자 오버로드 함수는, 자신이 가리키는 함수의 prototype을 typedef로 미리 만들어 놓고 반드시 typedef된 명칭으로 선언되어야 한다는 제약이 있다. 이것은 C++ 표준에도 공식적으로 명시되어 있는 제약이라 한다.

이런 어정쩡한 제약이 존재하는 이유는 아마도 함수 선언문에다가 다른 함수를 선언하는 문법까지 덧붙이려다 보니, 토큰의 나열이 너무 지저분해지고 컴파일러를 만들기도 힘들어서인 것 같다. 이 부분에서는 아마 C++ 위원회에서도 꽤 고민을 하지 않았을까.
안 그랬으면 형변환 연산자 함수의 prototype은 아래와 비슷한 괴상한 모양이 됐을 것이다. 실제로 이 함수의 full name을 undecorate한 결과는 이것처럼 나온다.

    operator void (*)(char)();

비주얼 C++에서는 함수를 저렇게 선언하면 그냥 * 부분에서 '문법 에러'라는 불친절한 말만 반복할 뿐이지만, xcode에 기본 내장되어 있는 최신형 llvm 컴파일러는 놀랍게도 나의 의도를 간파하더이다. “함수 포인터로 형변환하려면 반드시 typedef를 써야 합니다”라는 권고를 딱 하는 걸 보고 적지 않게 놀랐다. 이런 차이도 맥북을 안 쓰고 오로지 비주얼 C++ 안에서만 틀어박혀 지냈다면 경험하기 쉽지 않았을 것이다. 우왕~

() 연산자 오버로딩은 this 포인터가 존재하는 C++ 클래스 멤버 함수이며 static 형태가 있을 수 없다.
그러나 함수 포인터로의 형변환 연산자 오버로딩은 this가 없으며 C 스타일의 static 함수와 같은 위상이라는 차이가 존재한다.
두 오버로딩이 모두 존재하면 어떻게 될까? 혹시 모호성 오류라도 나는 걸까?

그런 개체에 함수 호출 ()가 적용되는 경우, () 연산자가 먼저 선택되며, 그게 없을 때에 한해서 함수 포인터 형변환이 차선책으로 선택된다. 모호성 오류가 나지는 않는다.
포인터 형변환 연산자와 [] 연산자가 같이 있을 때 개체에 배열 첨자 참조 []가 적용되는 경우, 역시 [] 가 먼저 선택되고 그게 없을 때 포인터 형변환이 차선으로 선택되는 것과 비슷한 맥락이라 볼 수 있다.

그래서 클래스와 연산자 오버로딩 덕분에 저런 문법이 가능해졌는데, C++11에서는 그걸로도 모자라 또 새로운 문법이 추가되었다. 이른바 람다 함수.

For_Each_Counterfeit(t, t+strlen(t), [](char x) { /* TODO: add your code here */ } );

람다 함수는 코드가 들어가야 할 곳에 함수나 클래스의 작명 따위를 신경쓰지 않고 코드 자체만을 직관적으로 곧장 집어넣기 위해 고안되었다. 세상에 C++에서 OCaml 같은 데서나 볼 수 있을 법한 개념이 들어가는 날이 오다니, 신기하지 않은가?

덕분에 C++은 C언어 같은 저수준 하드웨어 지향성에다가 성능과 이념을 적당히 절충한 수준의 객체지향을 가미했고, 90년대 중반에는 템플릿 메타프로그래밍 개념을 집어넣더니, 이제는 함수형 언어의 개념까지 맛보기로 도입한 가히 멀티 패러다임 짬뽕 언어가 되었다.

함수를 값처럼 표현하기 위해서 lambda 같은 예약어가 별도로 추가된 게 아니다. C/C++은 태생상 예약어를 함부로 추가하는 걸 별로 안 좋아하는 언어이다. (그 대신 문법에 혼동이 생기지 않는 한도 내에서 기호 짬뽕을 좋아하며 그래서 사람이나 컴파일러나 코드를 파싱하는 난이도도 덩달아 상승-_-) 보아하니 타입을 선언하는 부분에서는 배열 첨자가 먼저 오는 일이 결코 없기 때문에 []를 람다 함수 선언부로 사용했다.

람다 함수는 다른 변수에 대입되어서 두고두고 재활용이 가능하다. 그래서 C/C++에서는 전통적으로 가능하지 않은 걸로 여겨지는 함수 내부에서의 함수 중첩 선언을 이걸로 대체할 수 있다.

어떤 함수 안에서 특정 코드가 반복적으로 쓰이긴 하지만 별도의 함수로 떼어내기는 싫을 때가 있다. 굳이 함수 호출 오버헤드 때문이 아니더라도, 해당 코드가 그 함수 내부의 지역변수를 많이 쓰기 때문에 그걸 일일이 함수의 매개변수로 떼어내기가 귀찮아서 그런 것일 수도 있다.
이때 흔히 사용되는 방법은 그냥 #define 매크로 함수밖에 없었는데 이때도 람다 함수가 더 깔끔하고 좋은 해결책이 될 수 있다. 람다 함수는 선언할 때 캡처라 하여 주변의 다른 변수들을 참조하는 메커니즘도 언어 차원에서 제공하기 때문이다.

그렇다면 의문이 생긴다.
람다 함수는 그럼 완전히 새로운 type인가?
기존 C/C++에 존재하는 함수 포인터와는 어떤 관계일까?
정답부터 말하자면 이렇다. 람다 함수는 비록 어쩌다 보니 () 연산을 받아 주고 함수 포인터가 하는 일과 비슷한 일을 하게 됐지만, 활용 형태는 함수 포인터하고 아무 관련이 없으며 그보다 더 상위 계층의 개념이다.

템플릿과 연동해서 쓰인다는 점에서 알 수 있듯, 람다 함수는 함수 포인터와는 달리 calling convension (_stdcall, _cdecl, _pascal 나부랭이 기억하시는가?)이고 리턴값이 나발이고간에 아무 상관이 없다. 그저 코드상에서 함수를 값처럼 다루는 걸 돕기 위해 존재하는 추상적인 개념일 뿐이다. 뭔가 새로운 type이 아니기 때문에 람다 함수를 변수에다 지정할 때는 auto만을 쓸 수 있다. 즉, 다른 자료형이 아닌 람다에 대해서는 auto가 선택이 아니라 필수라는 뜻이다.

auto square=[](int x) { return x*x; };
int n = square(9); //81

square는 템플릿 같은 함수가 아니다. 이 함수의 리턴값은 x*x로부터 자동으로 int라고 유추되었을 뿐이다. [](int x) -> int 라고 명시적으로 리턴 타입을 지정해 줄 수도 있다. 구조체 포인터의 멤버 참조 연산자이던 -> 가 여기서 또 화려하게 변신을 한 셈임. 우와!

또한, sizeof(square)을 한다고 해서 포인터의 크기가 나오는 게 아니다. 사실, 람다 함수에다가 sizeof를 하는 건 void에다가 sizeof를 하는 것만큼이나 에러가 나와야 정상이 아닌가 싶다. 그런 개념하고는 아무 관계가 없기 때문이다.

람다는 함수 포인터가 아니기 때문에, square에다가 자신과 프로토타입이 같은 다른 람다 함수를 대입할 수 있는 건 아니다. 함수 포인터의 역할과 개념을 대체할 뿐, 그 직접적인 디테일한 기능을 대체하지는 못한다. 그렇기 때문에 콜백 함수를 받는 문맥에서

qsort(n, arrsize, sizeof(int), [](const void *a, const void *b) { return *((int*)a) - *((int*)b); } );

구닥다리 C 함수에다가 최신 C++11 문법이라니, 내가 생각해도 정말 변태 같은 극단적인 예이다만,
이런 식으로 람다 함수를 집어넣을 수도 없다.

요컨대 람다 함수는 코드의 추상화에 도움을 주고 종전의 함수 포인터 내지 #define, 콜백 함수 등의 역할을 대체할 수 있는 획기적인 개념이다. C++ 철학대로 디자인된 여타 C++ 라이브러리와 함께 사용하면 굉장한 활용 가능성이 있다. 그러나 이것은 함수 포인터에 대한 syntatic sugar는 절대 아니라는 걸 유의하면 되겠다.

Posted by 사무엘

2012/05/24 08:38 2012/05/24 08:38
, , ,
Response
No Trackback , 15 Comments
RSS :
http://moogi.new21.org/tc/rss/response/686

지금은 C++11이라고 개명된 C++ 확장 규격인 C++0x에는 잘 알다시피 여러 참신한 프로그래밍 요소들이 추가되었다. 몇 가지 예를 들면, 상당한 타이핑 수고를 덜어 줄 걸로 예상되는 auto 리뉴얼, 숫자와 포인터 사이의 모호성을 해소한 nullptr, 그리고 숫자와 enum 사이의 모호성을 해소한 enum class가 있다.

그런데 이것 말고도 C++11에는 아주 심오하고도 재미있는 개념이 하나 또 추가되었다. 복사 생성자에 이은 이동 생성자, 그리고 이를 지원하기 위한 type modifier인 &&이다. R-value 참조자라고 불린다. 이 글에서는 이것이 왜 도입되었는지를 실질적인 코드를 예를 들면서 설명하겠다.
다음은 생성자에서 주어진 문자열의 복사본을 보관하는 일만 하는 아주 간단한 클래스이다.

//typedef const char*  PCSTR;
class MyObject {
    PCSTR dat;
public:
    MyObject(PCSTR s): dat(strdup(s)) {}
    ~MyObject() { free( const_cast<PSTR>(dat) ); }
    operator PCSTR() const { return dat; }
};

C++은 언어 차원에서 포인터를 자동으로 관리해 주는 게 전혀 없다. 그렇기 때문에 저렇게만 달랑 짜 놓은 클래스는 함부로 값을 대입하거나 함수 호출 때 개체를 reference가 아닌 value로 넘겨 줬다간, 동일 메모리의 다중 해제 때문에 프로그램이 jot망하게 된다. C++ 프로그래머라면 누구라도 위의 코드의 문제를 즉시 알 수 있을 것이다.

그렇기 때문에, 포인터처럼 외부 자원을 따로 가리키는 클래스는 복사 생성자와 대입 연산자를 별도로 구현해 줘야 한다. 구현을 안 할 거면 하다못해 해당 함수들을 빈 껍데기만 private 형태로 정의해서 접근이 되지 않게 해 놓기라도 해야 안전하다.

MyObject(const MyObject& s): dat(strdup(s))
{
    puts("복사 생성자");
}
MyObject& operator=(const MyObject& s)
{
    free(dat); dat=strdup(s.dat); puts("복사 대입");
    return *this;
}

자, 그럼 이를 이용해 그 이름도 유명한 Swap 루틴을 구현해서 복사 생성자와 대입 연산자를 테스트해 보자.

template<typename T>
void Swap(T& a, T& b) { T c(a); a=b; b=c; }

int main()
{
    MyObject a("새마을호"), b("무궁화호");
    printf("%s(%X) %s(%X)\n", (PCSTR)a,(PCSTR)a, (PCSTR)b,(PCSTR)b);
    Swap(a,b);
    printf("%s(%X) %s(%X)\n", (PCSTR)a,(PCSTR)a, (PCSTR)b,(PCSTR)b);
    return 0;
}

프로그램의 실행 결과는 다음과 같은 식으로 나올 것이다.

새마을호(181380) 무궁화호(181390)
복사 생성자
복사 대입
복사 대입
무궁화호(1813B8) 새마을호(1813D0)

복사 생성자와 대입 연산자 덕분에 메모리 관리는 옳게 되었기 때문에, 이제 프로그램이 뻗는다거나 하지는 않는다.
그러나 이 방법은 비효율적인 면모가 있다. 개체의 값을 맞바꾸기 위한 세 번의 연산 작업 동안, 당연한 말이지만 메모리 할당과 해제, 그리고 문자열의 복사가 매번 발생했다. 그래서 비록 문자열 값은 동일하지만 그 문자열이 담긴 메모리 주소는 a와 b 모두 예전과는 완전히 다른 곳으로 바뀌었음을 알 수 있다.

이때 R-value 참조자를 쓰면, 이 클래스에 대해서 Swap 연산이 메모리를 일일이 재할당· 복사· 해제하는 게 아니라 a와 b가 가리키는 문자열 메모리 주소만 간편하게 맞바꾸도록 하는 언어적인 근간을 마련할 수 있다. 기존 참조자는 &로 표현하고, 이와 구분하기 위해 R-value 참조자는 &&로 표현된다. 참조자(&)는 포인터(*)와는 달리 다중 참조자(참조자의 참조자) 같은 개념이 없기 때문에, &&을 이런 식으로 활용해도 문법에 모호성이 생기지 않는다.

& 대신 &&를 이용해서 자신과 동일한 타입의 개체를 받아들이는 생성자와 대입 연산자를 추가로 정의할 수 있다. 이 경우, 이들 함수는 복사가 아닌 이동 생성자와 이동 대입 함수가 된다. 아래의 예를 보라.

MyObject(MyObject&& s)
{
    dat=s.dat, s.dat=NULL; puts("이동 생성자");
}
MyObject& operator=(MyObject&& s)
{
    //주의: 실제 코드라면 자기 자신에다가 대입하는 건 아닌지 체크하는
    //로직이 추가되어야 한다. if(&s!=this)일 때만 수행하도록.
    free(dat); dat=s.dat, s.dat=NULL; puts("이동 대입");
    return *this;
}

복사 버전과는 달리, strdup 함수 대신 그냥 포인터 대입을 썼음을 알 수 있다. 이것이 핵심이다.
그러면 s가 가리키던 메모리 영역이 내 것이 된다. 그 뒤 s가 가리키던 메모리는 NULL로 없애 줘야 한다. free 함수는 그 스펙상 자체적으로 NULL 체크를 하기 때문에, 소멸자 함수는 그대로 놔 둬도 된다.

즉, 이동 생성자와 이동 대입은 s의 값을 내 것으로 설정하긴 하나, 그 과정에서 필요하다면 s의 내부 상태를 건드려서 바꿔 놓을 수 있다. 그렇기 때문에 복사 생성자/대입과는 달리 s가 const 타입이 아니다.

이것만 선언해 줬다고 해서 Swap 함수의 동작 방식이 이동 연산으로 곧장 바뀌는 건 물론 아니다. 그랬다간 s의 상태가 바뀌고 프로그램 로직이 달라져 버리기 때문에, 컴파일러가 섣불리 동작을 바꿀 수 없다. 그렇기 때문에 Swap 함수의 코드도 move-aware하게 살짝 고쳐야 한다.

template<typename T>
void Swap(T& a, T& b)
{
    T c(static_cast<T&&>(a)); a=static_cast<T&&>(b); b=static_cast<T&&>(c);
}

즉, 개체를 생성하고 대입하는 곳에서, 가져오는 개체를 가능한 한 move로 취급하라고 명시적인 형변환을 해 줘야 한다. 이렇게 해 주고 나면 드디어 우리의 목표가 이뤄진다!

새마을호(181380) 무궁화호(181390)
이동 생성자
이동 대입
이동 대입
무궁화호(181390) 새마을호(181380)

물론, 저런 형변환 연산이 보기 싫은 사람은 <vector>에 정의되어 있는 std::move 함수로 이동 대입을 해도 되며, 보통 R-value 참조자를 설명해 놓은 인터넷 사이트들도 그 함수를 곧장 소개하고 있다. 하지만 그 함수의 언어적인 근거가 바로 이 문법이라는 건 알 필요가 있다.

생성이나 대입에서 R-value 참조자를 받지 않고 기존의 L-value 참조자만 받는 클래스에 대해서는, 이동 대입이나 생성도 자동으로 옛날처럼 복사 대입이나 생성 방식으로 행해진다.
다시 말해, Swap 함수의 로직을 저렇게 고치더라도 R-value 참조자가 구현되어 있지 않은 기존 타입들에 대한 동작은 전혀 바뀌지 않으며 컴파일 에러 같은 게 나지도 않는다. 그러니 호환성 걱정은 할 필요가 없다.

그리고 이미 눈치챈 분도 있겠지만, MFC의 CString처럼 자기가 가리키는 메모리에 대해서 자체적으로 reference counting을 하고 copy-on-modify 같은 테크닉을 구현해 놓았기 때문에, 어차피 복사 생성이나 call by value 때 무식한 오버헤드가 발생하지 않는 클래스라면, 구태여 이동 생성자나 이동 대입 연산자를 또 구현할 필요가 없다. 이동 생성/대입은 언제까지나 기존의 복사 생성/대입을 보조하기 위해서 도입되었기 때문이다.

특히 std::vector 같은 배열 컨테이너 클래스에다가 덩치 큰 개체를 집어넣거나 뺄 때 복사 생성자가 쓸데없는 오버헤드를 발생시키는 걸 막는 게 이 문법의 주 목적이다. 그렇기 때문에 딱히 smart한 복사 메커니즘을 갖추고 있지 않은 클래스를 STL 컨테이너에다 집어넣고 쓰는 C++ 코드라면, 적절한 이동 생성자와 대입 연산자를 구현해 주고 R-value 참조자를 지원하는 최신 C++ 컴파일러로 다시 빌드를 하는 것만으로도 성능 향상을 경험할 수 있다.

예전에는 배열 컨테이너 클래스들이 원소들의 일괄 삽입이나 삭제를 위해 무식한 memmove 함수를 내부적으로 쓰는 게 불가피했는데 이 역할을 이동 대입이 어느 정도 대체도 할 수 있게 됐다.
&&을 DLL symbol로 표기하기 위한 새로운 C++ type decoration도 별도로 물론 있다.

그런데 의문이 생긴다. &&의 이름이 왜 R-value 참조자인 것일까?
이 참조자는 참조자이긴 하지만, 오리지널 참조자처럼 L-value가 아니라 R-value를 취급하라고 만들어졌기 때문이다. L-value, R-value란 무엇인가? 대입문에서 좌변과 우변을 뜻한다. L-value란 값을 갖고 있으면서 동시에 대입의 대상이 될 수 있는 변수를 가리키며, R-value는 값을 표현할 수만 있지 그 자신이 다른 값으로 바뀔 수는 없는 상수, 혹은 임시 개체를 가리킨다고 보면 얼추 맞다.

아래의 코드에서 볼 수 있듯 기존 L-value 참조자는 dereference된 포인터와 같은 역할을 한다.

int& GetValue() { … }
GetValue() = 100;

int *GetValue() { … }
*GetValue() = 100;

그렇기 때문에 아래와 같은 특성도 존재한다.

void GetValue2(int& x) { x=… }

int a;
GetValue2(a); //a는 L-value이므로 OK
GetValue2(500); //에러. 당연한 귀결임

L-value 참조자가 상수값 내지 임시 생성 개체 같은 R-value를 함수의 인자로 받아들이려면, 해당 참조자는 const로 선언되어서 값의 변경이 함수 내부에서 발생하지 않는다는 보장이 되어야 한다. int&가 아니라 const int&로 말이다.

그런데 R-value 참조자는 const 속성 없이도 임시 개체나 상수값을 받아들이며, 그걸 뒤끝 없이 자유롭게 고칠 수 있다. 위의 GetValue2 함수가 int&&로 선언되었다면, 반대로 a를 전달한 게 에러가 나고 500을 전달한 건 괜찮다. a를 전달하려면 static_cast<int&&>(a)로 형변환을 해 줘야 한다. 그러면 마치 int&인 것처럼 실행되긴 한다.

R-value 참조자로 돌아온 함수의 리턴값은 말 그대로 R-value이기 때문에 대입 가능하지 않다. 그렇기 때문에 아래의 코드는 에러를 일으킨다. (R-value 참조자의 리턴값은 당연히 그 역시 R-value로 왔을 때에만 의미가 있을 것이다.)

int&& GetValue3() { … }
GetValue3() = 100; //에러

이런 R-value 참조자라는 괴상망측한 개념은 왜 도입된 것일까? 그리고 이게 앞서 이 글에서 언급한 이동 생성자/대입 연산하고는 도대체 무슨 관련이 있는 것일까?

R-value 참조자의 형태로 함수 인자로 넘어온 개체는 그 함수의 실행이 끝난 뒤엔 어차피 소멸되고 없어질 것이기 때문에 내부가 바뀌어도 상관없다. 즉, 이 참조자는 태생적으로 const 속성과는 어울리지 않는다. 오히려 const-ness가 보장되지 않아도 되는 제한적인 문맥에서, 쓸데없는 복사를 할 필요 없이 꼼수를 좀 더 합법적으로 구사할 수 있게 위해 이런 문법이 추가되었다고 보는 게 타당하다.

마지막으로 R-value 참조자가 유용하게 쓰이는 용도를 딱 하나만 더 소개하고 글을 맺겠다.
윈도우 API+MFC 기준으로, RECT 구조체를 받아서 이 값을 적당히 변형한 뒤에 이를 토대로 후처리를 하는 함수를 생각해 보자.

void Foo(const RECT& rc)
{
    RECT rc2 = rc; //rc는 const이기 때문에 복사본을 만들어야 함

    ::OffsetRect(&rc2, x,y); //변형
    ::DrawText(hDC, strMsg, -1, &rc2, 0);
}

void Foo(RECT&& rc)
{
    ::OffsetRect(&rc, x,y); //복사본 만들 필요 없이 rc를 곧바로 고쳐서 사용하면 됨
    ::DrawText(hDC, strMsg, -1, &rc, 0);
}

CRect r(100, 200, 400, 350);
Foo(r); //const RECT& 버전이 호출됨
Foo( CRect(0,0, 400,300) ); //임시 개체임. RECT&& 버전이 호출됨

RECT를 value로 전달했다면 당연히 복사가 일어나고, const reference로 전달했다면 역시 복사가 행해져야 한다. 그러나 애초에 함수에 전달되는 인자가 임시 개체였다면, 임시 개체에 대한 복사본을 또 만들 필요 없이 그냥 그 임시 개체를 바로 고쳐 쓰면 된다. 위의 코드의 의미가 이해가 되시겠는가?

R-value 참조자라는 게 왜 필요한지, 그리고 이게 왜 이동 생성/대입과 관계가 있는지 본인은 이해하는 데 굉장히 긴 시간이 걸렸다. 인터넷에 올라와 있는 다른 설명만 읽어서는 도통 이해가 되지 않아서 직접 코드를 돌리고 컴파일을 해 본 뒤에야 개념을 깨우쳤는데, 알고 나니 정말 이런 걸 생각해 낸 사람들은 천재라는 생각이 든다.;; C++은 참으로 복잡미묘한 언어이다.

Posted by 사무엘

2012/05/16 08:41 2012/05/16 08:41
,
Response
No Trackback , 15 Comments
RSS :
http://moogi.new21.org/tc/rss/response/683

델파이 (개발툴)

한 달쯤 전에 비주얼 베이직 리뷰를 쓴데 이어 오늘은 델파이와 해당 계열 RAD 툴의 리뷰를 좀 써 보겠다.
비주얼 베이직뿐만이 아니라 델파이와 C++ 빌더(C++ Builder)는 본인이 지금 같은 골수 비주얼 C++ 유저가 되기 전에, 도스에서 윈도우 프로그래밍으로 넘어가던 과도기 시절에 잠깐 써 본 개발툴이다. 고등학교 시절의 추억이 담겨 있다.

일단 파스칼이라는 언어 자체가 본인이 베이직에서 C/C++로 넘어가기 전에 과도기적으로 잠깐 공부했던 언어이다. 당시 정보 올림피아드 공부용으로 파스칼이 아주 깔끔하고 좋다는 말이 있기도 했고 말이다. 이 언어는 정말로 베이직과 C 사이의 과도기 역할을 하면서 본인의 프로그래밍 패러다임의 전환에 굉장한 도움을 주었다.

도스용 볼랜드 파스칼 역시 상당히 잘 만든 개발툴이었다. 그래서 본인은 이걸로 뭔가 이렇다할 프로그램을 개발해 보지 못한 게 좀 아쉽다. 개발툴의 본좌(?)이던 마이크로소프트와 볼랜드는 둘 모두 도스에서는 16비트의 한계를 벗어나질 못했으니 말이다. 그리고 지금 역시 <날개셋> 한글 입력기처럼 극도의 최적화를 추구해야 하는 프로그램은, 비주얼 C++만치 더 적격인 툴이 없는 것도 어쩔 수 없는 현실이다.

1990년대 초중반에 마이크로소프트가 '비주얼' 브랜드로 새로운 개발툴을 내놓은 것처럼 볼랜드는 오브젝트 파스칼 기반의 완전히 새로운 RAD 툴을 내놓았다. 그것이 바로 델파이. 게다가 1995년에 첫 출시된 1.0은 전무후무하게 16비트 윈도우용이었다.

델파이는 원래 AppBuilder라는 제품명이 붙을 예정이었고 Delphi는 코드명일 뿐이었다. 내 기억이 맞다면 이에 대해서 재미있는 일화가 전해진다.
잘 알다시피 IT계엔 그 이름도 유명한 Oracle이라는 데이터베이스 엔진(DBMS)가 있다. 이거 참 센스 있는 작명인게, DB에다가 SQL을 때려서 쿼리가 수행되는 것을 마치 신탁을 내리는 것에다 비유한 것이다. “수천만 개의 레코드 중에서 요것과 연계하여 이런 조건을 만족하는 놈을 눈앞에 0.1초 안에 대령하라.” 검색 엔진에다 심마니라는 이름을 붙인 것과 비슷한 맥락의 작명이라 하겠다.

그런데 신탁이 내려지는 곳이 어디던가? 신전이다. 그리고 고대 그리스에는 델파이라는 도시에 아폴로 신전이 있었다.
델파이는 DB와 연동하는 업무용 프로그램을 파스칼 언어를 기반으로 빠르고 편리하게 개발해 내라고 만들어진 개발툴이다. 그래서 DB 쿼리라는 신탁이 내려지는 장소에다 빗대어 델파이라는 코드명이 정해졌고, 이게 곧 제품명이 되었다. (뭐, 굳이 DB를 안 쓰더라도 각종 유틸리티나 에디터, 툴을 만드는 용도로도 좋지만 말이다.)

이런 이유로 인해, 델파이는 지금까지도 신전이나 집 비슷한 모양을 한 아이콘을 갖고 있다. 델파이의 C++ 버전이고 델파이보다는 훨씬 덜 유명한 C++ Builder는 집+크레인처럼 생긴 건축 기계 모양 아이콘이다. C++ 빌더는 다른 건 델파이와 비슷한데 역시 C++의 특성상 빌드 속도는 훨씬 더 느리며, RAD 툴의 용도에 맞게 C++ 문법을 자기 식대로 확장한 게 좀 있다. 또한 C++답게 경쟁사의 MFC 라이브러리도 내장하고는 있다.

그런 곳에서는 C++의 위상이 좀 므흣한 게, 닷넷으로 치면 마치 C++ managed extension 같은 존재이다. 닷넷에서는 아예 확실하게 C#을 쓰고 필요한 곳에나 unsafe 코드를 가끔씩 집어넣고 말지, 네이티브 기계어 개발이 아니라면야 C++이 얼마나 메리트가 있겠나 싶다. C/C++을 쓸 정도이면 아예 Win32 API만을 이용한 하드코어 저수준 개발을 하지, 애초에 RAD용으로 만들어진 게 아닌 언어에다가 그 정도로 추상화 계층을 거친 RAD 껍데기를 거추장스럽게 씌울 필요가 있겠나 하는 생각이다.

볼랜드에서는 자기네 RAD 툴에다가도 닷넷 기술을 연동하여 C# Builder 같은 툴을 만들기도 했지만 이건 얼마 못 가 접었다. 다들 비주얼 C#을 쓰지 굳이 볼랜드 툴을 쓰지 않았기 때문. 볼랜드는 그런 자신의 RAD 영역을 더욱 발전시켜서 마치 qt 같은 크로스 플랫폼 개발 프레임워크를 표방하며 리눅스용으로 카일릭스(Kylix)도 내놓고, 지금은 맥 OS X 범용 개발 환경도 내놓았는데, 아이디어는 분명 좋다만 결과는 과연 어떨까 궁금하다. 카일릭스는 수 년 전에 망했고 개발이 중단됐다.

하긴, 말이 나왔으니 말인데 얘들은 개발사의 명칭이나 주체가 여러 번 바뀌었다(개명· 인수 합병). 볼랜드이다가 한때는 Inprise, Codegear를 거쳐 지금은 Embarcadero임.

이런 저런 사정이 많았으나 델파이는 결국 오늘날까지도 그냥 윈도우 플랫폼 한정으로 강세인 것으로 보인다. 나름 네이티브 코드(오옷!)를 가히 C++로는 엄두를 못 낼 전광석화의 속도로 생성하는 RAD 툴이니, 생산성은 확실히 우위이다. 프레임웍에 속하는 코드가 단일 exe에 모조리 static 링크되어 들어가기 때문에, Hello world 급의 프로그램도 Release 빌드의 exe는 1MB 이상은 먹고 들어간다.

비주얼 C++ 2008 이상부터는 MFC를 static link해도 그 정도는 먹고 시작한다. 과거의 6.0 시절에는 MFC의 static link 오버헤드 크기가 200~300KB대였는데, 재미있게도 그 당시의 델파이 2~3도 exe의 기본 크기가 그 정도였으니 옛날이나 지금이나 오버헤드가 서로 비슷하다.

이런 이유로 인해, 델파이로 개발된 프로그램은 실행 파일을 실행 파일 압축기로 압축한 채 배포되는 경우가 종종 있다. 하지만 압축된 실행 파일은 코드 실행 영역이 동적으로 생성되고 고쳐지기 때문에, 동일 EXE가 중복 실행되었을 때 코드 영역이 동일한 물리 메모리를 공유하여 메모리를 절약하는 효과를 못 보지 싶다. 실행 파일 압축기가 집어넣어 준 압축 해제 stub이 그런 걸 똑똑하게 감지하여 처리하지 않는다면 말이다. 뭐, 요즘은 어차피 메모리도 차고 넘치는 시대이긴 하지만...;;

내 기억이 맞다면, C++ Builder는 델파이와는 달리 수 MB짜리 vcl.dll (Visual Component Library) 런타임이 필요한 작은 exe를 생성했었지 싶다. 즉, 정적 링크가 아니라 동적 링크 방식.

그런데 얘들의 프레임웍 라이브러리는 덩치만 큰 게 아니라 윈도우 API를 나름 체계적으로 잘 커버하고 있다. MFC는 윈도우 API에다 아주 최소한의 껍데기만 씌운 것에 가까운 반면, 볼랜드의 라이브러리는 운영체제 API에는 존재하지 않는 여러 추상적인 계층을 더 만들고, 심지어 같은 에디트 컨트롤도 single line (TEdit)과 multi line (TMemo) 버전을 따로 만들었다. MFC는 그냥 CEdit 하나로 끝인데 말이다. 내부 구현이 옵션만 다르게 지정된 동일한 에디트 컨트롤이니까 말이다.

라디오 버튼이나 체크 버튼도 under the hood는 그냥 버튼 컨트롤일 뿐이기 때문에 MFC는 CButton 하나로 끝이다. 그러나 볼랜드의 라이브러리는 응당 TRadioButton과 TCheckBox로 클래스가 따로 나뉘어 있다.
볼랜드의 프레임워크는 DC고 GDI 객체고 나발이고 생각할 것 없이 자기네가 마련한 TCanvas라는 개체를 통해 마음대로 색깔을 바꾸고 픽셀 단위 그래픽 접근이 가능한 반면, MFC에서는 그런 자비를 찾아볼 수 없다. 그런 추상화 계층을 마련하는 오버헤드가 exe의 실행 파일 크기 내지 런타임 DLL로 나타난다고 생각하면 됨.

이런 전통이 사실 볼랜드의 옛 C++ 라이브러리인 OWL (Object Windows Library)부터 어느 정도 전해져 오고 있었다. 델파이가 나오기 전, 볼랜드 C++/파스칼이 윈도우용으로 있던 시절 얘기이다. OWL이 좀 더 객체 지향 철학을 살려서 더 잘 만들어진 라이브러리이긴 했으나, 언제부턴가 IE가 넷스케이프를 누르듯이 MFC가 OWL을 떡실신시켜 버렸다.

세월이 세월이다 보니 델파이도 도움말 레퍼런스는 MS 비주얼 스튜디오의 Document Explorer를 쓰고 있어서 뜻밖이라는 생각이 들었다. 하긴, 옛날 버전은 아예 WinHelp를 쓰고 있었는데, 자기네만의 도움말 시스템을 새로 만드는 건 너무 뻘짓이고 그냥 chm을 쓰기엔 레퍼런스의 분량이 너무 방대한데, 저렇게 하는 게 나은 선택이다.

델파이의 근간 언어인 파스칼은 내부적으로 문자열을 포함하는 방식이 원래 C/C++과는 다르다. 그러나 운영체제의 각종 API들이 오로지 C/C++ 스타일의 null-terminated 문자열만을 취급하기 때문에 델파이 프로그래머도 C/C++ 스타일 문자열이라는 개념을 몰라서는 안 된다. 사실 파스칼과 C/C++은 함수 호출 규약조차도 달라서 과거에는 C/C++에서도 함수 선언할 때 STDCALL뿐만이 아니라 PASCAL이라는 속성이 있을 정도였다.

파스칼에도 포인터가 있긴 하다. 하지만 C/C++만치 배열과 포인터를 아무 구분 없이 남발할 수 있는 건 아니며 쓰임이 제한적이다. a[2]뿐만이 아니라 2[a]까지 가능한 건 가히 C/C++의 변태적인 특성이다만, 파스칼은 등장 초기에는 동적 배열이라는 개념 자체가 아예 없었다고 한다.
타입 선언에서 포인터를 의미하고 실제 수식에서는 포인터가 가리키는 값을 얻어오는 연산자가 C/C++은 *인데 파스칼은 ^이다.
그리고 이미 있는 변수의 주소를 얻어 오는 address-of 연산자는 C/C++은 &이고, 파스칼은 @이다.

델파이로 개발된 프로그램은 윈도우 비스타/7의 Aero 환경에서 창을 최소화해 보면 창이 작업 표시줄 쪽으로 미끄러지듯 fade out이 되지 않고 그냥 혼자 싹 없어지곤 했다. 나타나는 비주얼이 살짝 다르다. 델파이로 빌드된 다른 프로그램들을(특히 구버전) 살펴보면 차이를 알 수 있다.

그랬는데 최신 2011년도 델파이 XE2로 프로그램을 하나 빌드해 보니까 드디어 여타 프로그램처럼 제대로 최소화된다. 개선이 된 듯하다.
델파이가 유니코드와 64비트를 제대로 지원하기 시작한 것도 생각보다 최근이라고 들었다만.. 앞으로 이 툴이 어디까지 발전하고 MS의 비주얼 툴과는 다른 독자적인 지위를 유지할 수 있을지가 지켜보는 건 흥미로운 일일 것이다.

* 2014년 7월 1일 추가함.
델파이 1.0은 특정 업종 종사자들만 사용하는 딱딱한 개발툴인 주제에 무슨 게임을 방불케 하는 화려한 설치 화면을 자랑했다.

사용자 삽입 이미지

설치 프로그램이 full screen 모양으로 실행되는 게 유행이던 시절의 즐거운 추억이다. 본인도 중학생이던 시절 저 화면을 직접 본 적이 있다. 정말 개발툴 역사상 전무후무한 디자인이 아닐까 싶다.

속도계는 어떤 축적된 분량이 아니라 단위 시간당 변화량 개념이기 때문에,
굳이 이런 프로그램에 넣더라도 차라리 지금 파일의 전송 속도를 나타내는 용도가 더 적절할 것 같다만..;;
어쨌든 저 계기판에서 속도계가 전체 설치 진행 상황을 나타낸다.

굉장한 창의력과 잉여력이 아닐 수 없다. 그때는 MS Office 9x 프로그램에도 간단한 핀볼이나 3D 레이싱 게임이 이스터 에그로 들어가 있었을 정도이니 뭐...
단, 델파이의 경우 설치 중에 저 배경에서 차가 실제로 주행하여 배경이 입체적으로 스크롤된다거나 하지는 않는다. ^^ 그건 일말의 아쉬운 점이다.

Posted by 사무엘

2012/02/27 19:10 2012/02/27 19:10
, , ,
Response
No Trackback , 5 Comments
RSS :
http://moogi.new21.org/tc/rss/response/647

살다 보니 C#이나 파이썬도 아니고 비주얼 베이직으로 작성된 코드를 C++로 포팅해야 할 일이 있었다. C/C++로 갈아탄 뒤로는 베이직 코드는 다시는 볼 일이 없을 줄 알았는데 이거 정말 몇 년 만이냐.

들여다봐야 하는 코드는 닷넷도 아니고 비주얼 베이직 6으로 만들어진 코드였다. 하지만 GUI가 아니라 계산 알고리즘을 포팅하는 것이기 때문에 포팅이 크게 어려운 건 없었다. VB6에서 닷넷으로 넘어가면서 완전히 뒤집어엎어진 건 API 체계이지, 언어 자체가 그렇게 많이 바뀐 건 우려한 것만치 많지 않아 다행이었다. 자바와 자바스크립트의 관계에 필적하는 이질감은 아닌 것 같다.

언어가 바뀐 것은,
- 첫째, statement이던 것이 C 언어의 영향을 받아 다 일관된 함수 호출 형태로 바뀜. 그래서 매개변수 전체를 괄호로 싸야 됨. (파이썬도 3.0에서는 print가 statement에서 함수로 바뀜)

- 둘째, 타입이 예전보다 더 엄격해지고, 모든 변수는 반드시 사용 전에 선언을 해 줘야 함. 베이직은 원래 그런 걸 안 하는 언어이다가 하는 걸로 바뀌었다 보니, 변수를 선언하는 키워드가 Var이 아니라 Dim...이다. 원래는 배열을 선언할 때만 쓰는 키워드였지.
이런 추세를 정면으로 역행하는 GWBASIC의 잔재인 DefINT A-Z 같은 명령문은 당연히 퇴출이다.

- 셋째, 그리고 객체지향 패러다임에 맞춘 API의 전면 재구성이다. 예전엔 그냥 global 단위로 곧바로 호출하던 함수도 다 분야가 나뉘어서 클래스나 namespace에 소속된 메소드로 바뀌었다. 그래서 수학 함수도 바로 Sqrt라고 하면 안 되고 Math.Sqrt라고 써 줘야 하며, Math를 자주 쓴다면 Using 선언을 한 뒤에 생략해야 한다. 하긴, 비베에는 예전부터 With 키워드는 있긴 했다만.

이 정도.
요즘 언어들은 C/C++ 영향을 받아서 다들 대소문자 구분을 하는 게 유행이기 때문에, 혹시 비주얼 베이직도 그렇게 바뀌지 않았으려나 생각했다만...
의외로 명칭에 대소문자 구분을 안 하는 건 VB6이나 닷넷이나 마찬가지이다.

베이직은 원래 좀 가볍고 동적인 언어였는데, MS의 닷넷 입맛대로 대수술을 거치다 보니 그냥 C#의 표현력에 필적하는 전형적인 절차형 언어가 된 것 같은 느낌이 든다. 예전의 베이직 같은 느낌은 파이썬이 더 잘 간직하고 있는 듯.

배열 첨자도 ()로 싸고, 함수 호출 인자도 ()로 싸는 건 베이직의 특징이다. 언뜻 보기에 굉장히 혼동될 것 같은데 C++처럼 [] () 따로 연산자 오버로딩이라도 해야 하는 게 아니라면 의외로 둘이 문법 차원에서 혼동될 일은 없다. 참고로 본인은 ::와 .의 구분이 없는 객체지향 언어들에 대해서도 의아해한 적이 있었는데 이 구분 역시, 포인터만 없다면 거의 필요하지 않다.

그러고 보니 베이직은 대입도 =, 동등 비교도 =이다. A=B=1이라고 하면 C언어 식으로 치자면 A=B==1처럼 해석된다. 원래 베이직의 대입문은 Let A=1 처럼 써 줘야 맞는데 Let이 C언어의 auto만큼이나 캐잉여로 전락하는 바람에 지금 같은 꼴이 된 것이다. 그러고 보니 Let을 Dim처럼 변수 선언 키워드... 아니 C++0x의 auto처럼 쓰는 것도 괜찮을 것 같다?

Dim A as Double
Dim I as Integer

뿐만 아니라

Let A = 0.52 '자동으로 실수 확정
Let I = 5    ' 자동으로 정수 확정

이렇게도 되게 말이다. 타입이 이랬다저랬다 바뀌는 Variant가 아님. 기발하지 않은지? ㄲㄲ

베이직처럼 구문 분석이 쉬운 언어는 IDE의 인텔리센스나 코드 자동 완성 같은 기능이 C++의 그것과는 비교할 수 없이 안드로메다급으로 훨씬 더 빠르고 똑똑하고, 돌아가는 게 손에 착착 달라붙는다. 도스 시절의 퀵베이직이 이미 인텔리센스만 없을 뿐이지 그런 꿈의 프로그램 개발 환경을 어느 정도 제공하고 있었다. 빌드 속도는 두말 할 나위도 없음.

그러나 C++ 코드는 실시간으로 코드 변경 사항을 IDE가 따라잡으려면, 비주얼 스튜디오든 Source Insight든, 어쨌든 background에서 소스를 다 까 보는 작업을 하지 않으면 안 된다. 그래서 어떻게든 처리 속도를 올리려고 덕지덕지 남기는 부가 정보 데이터가 많다. 함수 이름을 바꾼 게 C/C++은 복잡한 절차를 거쳐 최소한 수 초 뒤에 IDE의 ClassView에 반영되는 반면, 베이직은 ‘즉시’이다.

이런 생산성과, C++ 특유의 졸라 가볍고 효율적인 네이티브 코드라는 두 마리 토끼를 모두 잡은 프로그래밍 언어 + 개발 환경은 정녕 없는 것일까.
하긴, 윈도우 환경에서 베이직 언어로 네이티브 코드를 생성하는 컴파일러는 MS 제품 중에는 없고 파워베이직이라는 브랜드가 있다. 하지만 인지도가 안습한 수준.

Posted by 사무엘

2012/01/26 08:45 2012/01/26 08:45
,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/632

예전에 본인이 글로 쓴 적도 있고, 상식 차원에서 이미 아시는 분도 있겠지만..
프로그래밍 언어마다 문자열을 다루는 방식엔 차이가 존재한다.
C/C++은 null-terminated 문자열이라는 단순하고 독특한 체계를 사용하는 반면, 다른 언어들은 그렇지 않다.
그렇기 때문에, 문자열 상수가 실행 파일 내부에 어떤 형태로 박혀 있는지를 추적하면, 이 프로그램이 무슨 언어로 만들어졌겠는지 추측이 어느 정도 가능하다.

과거의 도스 시절에는 볼랜드 사에서 개발한 터보 시리즈의 컴파일러가 인기가 많았다. C/C++과 파스칼이 기억에 남는다. 이 볼랜드 제품은 당시 타사의 컴파일러가 제공하지 않던 두 가지 독자적인 기능이 있었다. 하나는 깔끔하게 잘 만들어진 IDE(에디터)였고, 다른 하나는 BGI(볼랜드 그래픽 인터페이스)라고 일컬어지는 그래픽 API였다.

한 IDE에서 프로그램을 바로 빌드-실행-디버그할 수 있으니 프로그램 개발 생산성이 뛰어나고 굉장히 편리하다. 이에 덧붙여, 그래픽은 그렇잖아도 printf 같은 표준화된 API 규격이 전무해서 ‘싸제’ 라이브러리에 의존할 수밖에 없던 영역인데, 자체 개발 라이브러리가 있다 보니 볼랜드의 컴파일러는 폭발적인 인기를 모을 수밖에 없었다.
bgidemo라고 유명한 그래픽 API 예제 프로그램도 있었는데 기억하는 분이 있으려나 모르겠다. QBasic용 예제 프로그램인 nibbles, gorilla 게임과 비슷한 시기에 만들어진 그 시절 추억이다.

사용자 삽입 이미지

아래의 스크린샷은 이 BGI 라이브러리를 사용해서(=링크해서) 만들어진 어느 EXE 파일 내부를 들여다본 모습이다. 그래픽 라이브러리이다 보니 내부적으로 출력하는 에러 메시지 문자열, 가령 No error, (BGI) graphics not installed, 심지어 Out of memory in flood fill 같은 친숙한 문자열이 내장되어 있음을 알 수 있다. 그런데 동일한 문자열들 사이에 한 놈은 ▲, →, ← 같은 이상한 기호가 듬성듬성 끼어 들어가 있다. 왜 그럴까?

사용자 삽입 이미지

사용자 삽입 이미지

기호가 없는 프로그램은 C언어(=터보 C)로 만들어진 프로그램이다. 왼쪽의 16진수값을 보면 알겠지만, 이들은 모든 문자열들이 그냥 0번 문자로 구분되어 있다.
그러나 기호가 있는 프로그램은 파스칼로 만들어진 프로그램이다. ▲, →, ←은 다음에 뒤따르는 문자열의 길이를 의미한다. 예를 들어 “▲Graphics hardware not detected”를 보면 ▲의 코드 번호는 0x1E, 즉 30인데 그 에러 메시지의 길이는 30바이트임을 알 수 있다. 얘네는 반대로 문자열들 사이에 0번 문자가 전혀 존재하지 않는다.

실제로 C/C++ 말고 String이 built-in type으로 존재하는 언어들은 이렇게 글자 수를 따로 저장해 놓는 방식으로 문자열들을 관리한다. 베이직으로 만들어진 프로그램도 QuickBasic이든 PowerBasic이든 문자열 상수들을 들여다보면 비슷한 결과를 얻을 수 있다. 그래서 이런 언어는 문자열의 길이를 구하는 함수의 시간 복잡도가 O(1)인 반면, C언어만 strlen의 시간 복잡도는 O(n)이다.

베이직 언어들은 문자열의 길이가 16비트 정수로 저장되던 반면, 터보 파스칼은 문자열 길이를 달랑 8비트 크기로 저장하여, 문자열의 길이가 256자를 넘을 수 없다는 한계가 존재했다. 흠;;

파스칼로 만든 프로그램을 들여다보면 Runtime error 같은 문자열도 존재한다. 이 역시 C/C++로 만들어진 프로그램에서는 디버그 빌드가 아닌 이상 있을 수 없는 개념이다. C/C++은 배열 첨자 범위의 검사조차도 안 할 정도로 런타임 에러라는 개념 자체가 존재하지 않는-_- 언어이기 때문이다. 그저 컴퓨터 다운(도스 시절)이 아니면 segmentation/page fault(요즘 같은 보호 모드 운영체제에서)-_-만이 존재할 뿐. -_-;;

그 반면, %d, %s이라든가 Null pointer assignment 같은 문자열이 있다면 그건 99.9% C 라이브러리가 들어갔다는 뜻이고 그 프로그램은 C/C++로 작성되었다고 유추할 수 있다.

덧붙이는 말

1. 볼랜드는 BGI 라이브러리만큼이나 텍스트 모드용 GUI? TUI? 툴킷으로 Turbo Vision이라는 라이브러리를 개발한 것으로도 유명했다. MS가 도스용 비주얼 베이직을 잠시나마 개발했다면 볼랜드에는 이런 게 있었던 셈. 당장 터보 C++과 파스칼의 IDE부터가 이를 사용해서 개발되기 시작했다. 비록 C/C++과 파스칼에서 모두 지원되긴 했지만 이 언어의 주 개발 및 지원 언어는 파스칼이었지 싶다. MS가 베이직을 좋아한다면, 볼랜드는 전통적으로 파스칼을 더 좋아하는 회사였다. (그러니까 훗날 델파이까지 만들었지)

지금은 세월이 세월이다 보니 소스가 완전히 풀려서 이이 프로젝트는 오픈소스 진영에서 관리되고 있다. 내 기억이 맞다면 DJGPP의 IDE인 Rhide가 이 Turbo Vision의 오픈소스 버전으로 개발되었다.
그리고 우리나라에서 PC 경진대회가 정보 올림피아드로 최초로 바뀌었던 1996년(13회), 대회의 채점 프로그램이 Turbo Vision 기반으로 개발되어 있던 걸 본인은 분명히 봤다.

2. 오늘날 윈도우용 네이티브 EXE/DLL이 만들어지는 출처는, 내 감으로는 비주얼 C++이 적게 잡아도 70% 이상, 그 뒤에 소수의 오픈소스 프로젝트용으로 gcc, 그리고 끝으로 델파이 정도가 고작인 것 같다. 볼랜드는 그 후로 다른 회사에 인수되면서 이름도 여러 번 바뀌고(InPrise, CodeGear, Embarcadero 등...;;) 우여곡절을 많이 겪었는데 걔네 입장에서는 옛날의 영광이 그리울 법도 할 것 같다.

3. BGI 라이브러리와 파워베이직--얘 역시 전신이 볼랜드 사의 터보 베이직이긴 했지만--의 그래픽 라이브러리는 이상하게도 VGA mode 13h를 지원하지 않아서 개인적으로 아쉬웠었다. (퀵베이직은 지원했는데...) 해상도가 너무 낮아서 한글· 한자 같은 문자를 찍는 데는 부적격이었지만 256색 덕분에 게임 만들 때는 필수이던 그래픽 모드이다. 그게 지원됐으면 그 당시 게임 만들기가 훨씬 더 수월했을 텐데 말이다.

Posted by 사무엘

2011/07/15 08:38 2011/07/15 08:38
, , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/540

C/C++로 프로그램을 개발하는 과정에서 아주 난감해지는 경우 중 하나는, 바로 Debug 빌드와 Release 빌드의 실행 결과가 서로 다를 때이다. 개발 중이던 Debug 빌드 스냅샷에서는 잘만 돌아가는 프로그램이 정작 최적화된 Release 빌드에서는 이따금씩(항상도 아니고!) 에러가 난다면?

이런 버그는 문제를 찾아내려고 정작 디버거를 붙여서 실행할 때는 재연되지 않는 경우가 태반이어서 프로그래머를 더욱 애먹인다. 특히 복잡한 멀티스레드와 관련된 버그라면 그저 묵념뿐..;; 하지만 그런 특수한 경우가 아니라면, Debug와 Release의 실행 결과가 다른 이유는 본인의 경험상 거의 대부분이 초기화되지 않은 변수 때문이었다.

비주얼 C++은 Debug 빌드에서는 초기화되지 않은(공간 확보만 해 놓고 프로그램이 아직 건드리지는 않은) 메모리의 영역을 티가 나는 값으로 미리 표시도 해 놓고 아주 특수하게 취급해 준다. 메모리를 할당해도 좌우에 여분을 두고 좀 넉넉하게 할당하며, 때로는 그 넉넉한 여분 공간의 값이 바뀐 것을 감지하여(바뀌어서는 안 되는데) 배열 첨자 초과 같은 에러를 알려 주기도 한다. 프로그래머의 입장에서야 이건 꽤 유용한 기능이다.

그러나 Release 빌드에는 이런 거추장스러운 작업이 물론 전혀 없다. 그러니 메모리 범위를 초과한다거나, 읽어서는 안 되는 엉뚱한 주소의 메모리로부터 값을 읽거나, 올바른 영역이더라도 초기화되지 않은 쓰레기 값을 얻었을 때의 결과는 두 빌드가 서로 극과 극으로 달라질 수밖에 없다.

이렇게, 빌드 configuration에 따라 동작이 달라지는 코드는 두말 할 나위도 없이 결함이 들어있는 faulty 코드이다. 이런 코드에서 문제의 원인을 찾는 건 극도로 어려운 일이다. 서울에서 김 서방 찾기, 모래사장에서 바늘 찾기, 사격장에서 흘린 탄피 찾기가 따로 없다. ㅜㅜ 자기가 짠 코드에서 결함을 찾는 것도 어려워 죽겠는데 하물며 회사 같은 데서 남이 짠 faulty 코드를 인수인계 받았다면... -_-;;;

(본인이 다니던 모 병특 회사에서 본인의 직속 상사는 이렇게 말했다. “그런 코드를 짜는 건 프로그래밍을 하는 게 아니라 똥을 싸는 거다.” 공감한다. -_-)

C/C++은 물론 간단한 지역 변수에 대해서야 ‘이 변수를 초기화하지 않고 사용했습니다’ 같은 지적을 컴파일 시점에서 해 준다. 그러나 복잡한 포인터나 배열로 가면 일일이 그 용법이 올바른지 컴파일 시점에서 판단하지는 못한다. 그저 프로그래머가 조심해서 코드를 작성하는 수밖에 없다.

이와 관련된 본인의 경험을 소개하겠다.
꽤 옛날에 짜 놓은 비주얼 C++ MFC 기반 GUI 프로그램 소스의 내부에서, 핵심 알고리즘만 떼어내서 다른 콘솔 프로그램에다 붙여야 할 일이 있었다.
그 당시에는 나름 구조적으로 프로그램을 만든 것이지만, 지금 관점에서 모듈간의 cohesion은 여전히 개판오분전이었던지라 상당수의 코드를 리팩터링해야 했다.

그래서 코드를 붙였는데, 원래의 GUI 프로그램에서는 잘 돌아가던 코드가 새로운 프로젝트에서는 얼마 못 가서 뻗어 버렸다. Debug 빌드와 Release 빌드의 실행 결과가 다른 건 두말 할 나위도 없거니와, 심지어 같은 Release 빌드도 F5 디버거를 붙여서 실행하면 별 탈이 없는데 그냥 실행하면 뻗었다! 이건 스레드 쓰는 프로그램도 아닌데! 이거야말로 제일 골치 아픈 경우가 아닐 수 없었다.

Debug 빌드는 Release 빌드보다 워낙 느리게 돌아가고, Release 빌드도 디버거를 붙였을 때와 그렇지 않았을 때 성능이 살짝 달라진다. 그러니 앞에서 언급했듯이 스레드 관련 race condition은 영향을 받을 수 있다. 하지만 그런 것도 아니라면? 의심스러운 배열은 무조건 다 0으로 초기화하고, 혹시 내가 리팩터링을 하면서 실수를 하지는 않았는지 몇 번이나 꼼꼼이 살펴봤지만 문제는 눈에 띄지 않았다.

별 수 있나. printf 로그를 곳곳에다 박아 넣어서 의심스러운 부분을 추적한 뒤 다행히 문제를 찾아냈다.
게임 같은 리얼타임 시스템에서는, 심지어 디버그 로그 찍는 코드만 추가해도 버그가 쏙 숨바꼭질을 해 버리는 막장 중의 막장 경우도 있다만 내 프로그램은 그런 정도는 아니어서리..;;

사실은 기존 GUI 프로그램에서 돌아가던 코드에서부터 문제가 있었다.
배열을 선언했는데, 0~1번 인덱스에 접근할 일이 없어서

ptrData = new char[100];
ptrData-=2;

같은 잔머리를 굴려 줬던 것이다. 요런 짓을 옛날에 Deap 자료구조를 구현할 때도 했던 것 같다.
그러니 이 포인터로는 0과 1번 인덱스를 건드리지 않아야 하는데...
그런데 그것이 실제로 일어났습니다. ㄲㄲㄲㄲㄲ

그 허용되지 않는 메모리의 상태가 GUI 프로그램과 콘솔 프로그램, 심지어 같은 프로그램도 Debug와 Release, 디버거 붙이냐 안 붙이냐 여부에 따라 싹 달라져서 나를 골탕먹였던 것이다. 예전에는 수 년째 아무 탈 없이 잘 돌아가던 코드가 말이다.
저런 간단하고 고전적인 배열 첨자 초과 문제가 이런 결과를 야기할 줄 누가 알았을까?

C/C++은 내가 짠 코드를 내가 완전히 책임질 수 있고 컴퓨터 관점에서의 성능· 능률· 최적화가 중요한 해커나 컴덕후에게는 가히 환상적인 언어이다. 이보다 더 좋을 수가 없다. 예전에 내가 비유했듯, 세벌식이 기계 능률과 인체 공학적인 특징을 잘 살린 것만큼이나 이 언어는 고급 언어의 특성과 기계적인 특성을 꽤-_- 잘 절충했다.

그러나 언어의 구조적으로 가능한 무질서도가 너무 높은 것도 사실. C/C++가 까이는 면모 자체가 크게 (1) 언어 자체의 복잡도 내지 결함 그리고 (2) unmanaged 환경이라는 여건 자체라는 두 갈래로 나뉘는 양상을 보인다. 오늘날의 소프트웨어 시스템에서 프로그래밍 언어는 모름지기 수십, 수백만 줄의 프로젝트에서 살인적인 복잡도를 제어 가능해야 하고, 작성한 코드의 최소한의 품질과 안전성이 보장되어야 하며, 또 무엇보다도 빨리빨리 빌드가 돼야 하는데 C/C++은 영 한계를 보이기도 한다.

뭐, 그래도 이미 C/C++로 작성된 코드가 너-_-무 많고 그것도 다들 중요한 저수준 계층에 있다 보니, 이 언어가 쉽게 없어지지는 않을 것이고 특히 C++은 몰라도 C는 절대 안 없어지리라.. ㅋㅋ 프로그래밍 언어의 라틴어급.

C/C++과는 전혀 다른 언어이다만, 과거엔 QuickBasic도 IDE에서 돌리는 프로그램과, 실제로 컴파일-링크를 한 EXE의 실행 모습이 대동소이하게 달라서 프로그래머를 애먹이기도 했다. 물론 이건 C/C++에서의 Debug/Release와는 다른 양상 때문에 차이가 나는 경우이다.
결론은, 프로그램 작성하다가도 틈틈이 Release 형태로 최종 결과물을 확인하는 게 필요하다. ^^

Posted by 사무엘

2011/06/22 08:23 2011/06/22 08:23
,
Response
No Trackback , 6 Comments
RSS :
http://moogi.new21.org/tc/rss/response/529

C/C++에는 ? : 라는 독특한 연산자가 있다. A ? B: C꼴로 표현되어 피연산자가 3개나 붙는 유일한 연산자이다.
이 연산자의 역할은 매우 단순하다. A가 참이면 연산자의 값은 B가 되고, 그렇지 않으면 C가 된다. 그래서 아예 if문의 역할을 간단히 대신할 수도 있으며, 콤마 연산자와 결합하면 어지간한 함수 호출마저도 한 연산식에다 박아 넣을 수 있다. 다만, 그게 너무 사악하다고 여겨졌는지-_-, C# 언어에는 콤마 연산자가 사라지고 콤마는 for 키워드 안에서만 제한적으로나 허용되지 싶다.

? : 는 &&, || 와 마찬가지로 C/C++에서 단축연산이 적용된다. A && B에서 A가 거짓이면 B는 실행이 전혀 되지 않고 전체 결과가 거짓이 되며, A || B에서 A가 참이면 B는 실행되지 않고 바로 전체 결과가 참이 된다. 그런 것처럼 ? :는 선택되지 않은 항에 대해서는 당연히 연산이 일어나지 않는다.

<날개셋> 한글 입력기는 짝퉁 C언어 문법 수식 해석기를 내장하고 있기 때문에, 이를 이용해 글쇠, 오토마타, 글자판 전환 글쇠 등에서 문자 입력 시스템의 자유도를 굉장히 높일 수 있다. 비록 튜링 완전한 수준은 못 돼도 말이다. 이때에도 ? : 연산자는 물론 매우 요긴하게 쓰인다.

? : 는 좌결합이 아니라 우결합이다. A ? B : C ? D : E는 (A?B:C) ? D : E가 아니라 A ? B : (C?D:E)로 결합한다. 그러므로 전자처럼 쓰려면 괄호를 넣어 줘야 한다.

? : 는 다른 연산 구문들을 포함하는 if문 대용처럼 쓰이는 만큼, 연산자의 우선순위가 상당히 낮다. 다른 평범한 연산자들이 다 결합한 뒤 나중에야 적용된다. 그게 합리적이다.
그러나 얘도 콤마와 대입 연산자보다는 순위가 높다. 그렇기 때문에 A = B ? C : D 라고 써 주면 알아서 A = (B?C:D)로 해석되어, A에는 B 조건의 충족 여부에 따라 C 아니면 D가 대입된다.

반대로, ? : 의 내부에 콤마 연산이나 대입 연산이 포함되어야 한다면 이들 연산은 무조건 괄호로 싸야 한다.

A ? (B=2): (C=5)
B에다가 괄호를 안 하면 = 가 ?와 :를 둘로 쪼개 버리는 효과가 나기 때문에 에러가 발생한다.
그리고 C에다가도 괄호를 생략할 수 없는데, 괄호를 안 하면 연산의 의미가 (A?(B=2):C)=5가 되어 버리기 때문이다. 우선순위의 특성상, =가 C항이 아니라 ? = 전체와 대응한다는 뜻 되겠다.

그리고 또 생각해 볼 것은, ? : 연산자의 값은 L-value가 될 수 있겠냐는 점이다. (대입 가능하겠냐)
<날개셋> 한글 입력기는 수식이 처음 도입된 3.0 이래로 지금까지 (조건 ? A:B)=100 과 같은 구문이 지원된 적은 없다. 그러나 이제 <날개셋> 6.0 이후의 다음 버전부터는 그게 가능해진다. 단, 2항과 3항 중 하나라도 변수에 연산자가 조금이라도 붙어서 A+2, -B 같은 형태가 되면 L-value 원칙이 깨지게 되는데, 그런 오류는 수식 입력 시점에서 프로그램이 자동으로 감지해 준다.

이게 지원되면 조건 ? (A=100): (B=100)보다야 구문을 더욱 간단하게 만들 수 있으니까 사용자의 입장에서 좋을 것이다. 더구나 콤마 연산자도 최후의 항의 변수 정보를 남겨 주기 때문에 (조건 ? (A=100,C): (B=50,D)) +=20 같은 복잡한 대입도 가능해진다. 저 식의 의미는 무엇일지 독자 여러분이 생각해 보기 바란다.

정작 이 연산자에서는 괄호가 필요하지 않다. 조건 ? A:B=100 이라고 하면 (조건 ? A:B)=100이 되며, 100 대입 연산은 3항의 B에만 연결되는 게 아니라 ? : 연산의 결과 전체에 걸린다. ? : 의 우선순위가 =보다 높기 때문에 =보다 먼저 계산되기 때문이다.

<날개셋> 한글 입력기로 복잡한 수식을 다뤄 본 분들은 이미 아시겠지만, 이 프로그램은 사용자가 입력한 수식을 어느 정도 자동으로 간소화를 한다. 상수 연산은 미리 계산을 해 버리며, 100/0나 2=A 같은 뻔한 에러는 미리 지적해 준다. 그리고 우선순위 규정상 굳이 칠 필요가 없는 괄호도 알아서 제거를 해 버린다.

(A+B)-C는 A+B-C로 바뀌며, 이와 비슷한 맥락으로 (조건 ? A:B)=100도 그냥 조건 ? A:B=100으로 바꾼다. 이건 프로그램의 오동작이 아니므로 놀라지 말고 수식을 사용하면 된다.

그런데 비주얼 C++ 같은 요즘의 C/C++ 컴파일러들은 ? :를 본인이 생각한 것처럼 취급하지 않는 것 같다.
A==100 ?B:C=400 라고 하면 =400은 3항의 C에만 붙지 B에는 붙지 않는다. (A==100 ? B:C)=400이라고 해 줘야 한다.
또한 ?와 : 사이에 있는 2항은 사이에 대입이나 콤마 같은 연산자(우선순위가 ? :보다 한참 더 낮은!)가 괄호 없이 연결되어 있어도 알아서 2항의 일부라고 인식해 주는 듯.
물론, 그렇다고 해서 A=조건 ? 2항: 3항 같은 문장이 있으면 A=까지 조건으로 끌어들이지는 않는다.

이런 세세한 동작 방식에 대해서 정보를 얻고 싶어서 비주얼 C++ 도움말을 찾아봐도, ? :는 대입 연산자보다 우선순위가 높다던가, 2항과 3항의 타입이 서로 다를 때 연산자 값이 정해지는 원칙 같은 원론적인 말밖에 없다. 그 말대로라면 무조건 내 프로그램처럼 괄호를 써야만 할 텐데 말이다.

그 간단한 ? : 연산자에도 의외로 복잡한 사연이 있다는 걸 알 수 있다.
어쨌든 내 프로그램은 ? : 안에 대입이나 콤마 연산을 포함시키려면 무조건 괄호를 써야만 하는 구조가 앞으로도 유지될 것이다.

Posted by 사무엘

2011/06/05 19:20 2011/06/05 19:20
, ,
Response
No Trackback , 4 Comments
RSS :
http://moogi.new21.org/tc/rss/response/521

파이썬 언어

요즘 개인적으로 파이썬을 틈틈이 공부하고 있는데, 나름 재미있다. 대략 20세기 말쯤에 우리나라에 파이썬이 얼리어답터 선구자들에 의해 처음으로 대대적으로 소개됐을 때는, Python의 한글 표기조차도 통일이 안 돼 있었다고 하니 참으로 격세지감이다. 본인은 처음부터 일관되게 파이썬이라고만 들었다.

파이썬이라는 언어가 있다는 걸 본인이 안 건 굉장히 오래 됐다. 거의 2001~2002년 사이인데, 당시 세벌식 사랑 모임에서 '컴바치'라는 필명을 쓰던 송 시중 님과 얘기를 나누다가 파이썬에 대해 처음으로 들었다. 이분, 연락이 끊어진 지는 굉장히 오래 됐는데, 지금은 뭘 하고 계시는지 모르겠다.

그 후 본인은 학교 후배로부터도 파이썬을 좀 공부하는 게 어떻냐는 권유를 몇 차례 받았다. 하지만 오로지 C++ 만능주의에 <날개셋> 한글 입력기 개발에만 정신이 팔려 있던 본인은, “난 비주얼 C++만 있으면 컴퓨터를 내가 원하는 대로 얼마든지 부려 쓸 수 있는데, 그거 또 배워서 뭐 함?” 식으로 별 흥미를 느끼지 못했다. 난 전산학 전공자치고는 컴퓨터 다루는 형태가 아주 기괴하다. -_-;;

그로부터도 또 수 년이 지나고, 무려 대학원에 가서야 본인은 드디어 파이썬을 다시 대면하게 됐다. 파이썬이 말뭉치 같은 대용량 텍스트 데이터를 다루는 도구로서, 전산 비전공자도 쉽게 배울 수 있는 언어로 즐겨 쓰이고 있었던 것이다.

나는 문과 기반이 부족하니 그런 걸 주변 선배들로부터 보충받고, 반대로 전산학 기반이 아주 탄탄하기 때문에 그런 걸 전수해 주는 쪽으로 협업 구도가 자연스럽게 형성되었다. 파이썬 좀 가르쳐 달라는 요청이 있기도 했으니, 본인은 남을 가르치기 위해서 내 자신부터 파이썬을 공부하게 됐다.

한동안 공부해 본 소감은... 파이썬은 꽤 재미있는 언어이다!
type이 runtime 때 동적으로 결정되고 무척 유동적이라는 것은 C++ 특유의 그 경직된 사고방식으로부터 해방감을 느끼게 해 줬다.

{ } 일색인 C/C++, 자바, C# 같은 언어하고만 놀다가...
들여쓰기가 필수 조건이고 for/while/def :로 끝난다는 언어를 접하니 느낌이 새롭다. 좀 베이직과 비슷하다는 생각도 든다. 물론 그렇다고 행번호+GOTO 스파게티 같은 건 전혀 없지만.

다중 대입 기능이라든가 리스트의 slicing 연산은 무척 편리하고 좋았다.
여타 언어였다면 또 임시 변수를 동원한다거나, 번거로운 개체 생성과 반복문이 필요했을 것이다.
C/C++, 자바, C#의 for문은 while문을 형태만 바꾼 것과 완전히 동치이지만, 파이썬의 for 문은 철저하게 복합 자료형의 각 원소를 순회하는 기능에 맞춰져 있다. for문의 설계 철학은 C스타일 언어와 베이직/파스칼 스타일 언어, 그리고 파이썬도 살짝 차이가 있는 것 같다.

언어와 내 사고방식이 완전히 일심동체가 되기 위해서는,
- 리스트 같은 복합 자료형이 내부적으로 구현되는 실제 자료 구조는 무엇이며 시간 복잡도가 얼마나 되는가? 메모리 재할당 비용이 얼마나 되는가?
- 대용량의 복합 자료형을 만들어서 복제하거나 함수 인자로 전달했을 때 shallow copy가 일어나는가, deep copy가 일어나는가?

이런 식의 디테일을 알 필요가 있다.
이것도 몇 번 튜토리얼을 읽고 예제 코드를 짜면서 시행 착오를 겪어 보니 그리 어렵지 않게 이해가 됐다.
문자열과 튜플은 새로운 값의 생성과 대입/재대입만 가능하지, 이미 만들어진 값의 변경은 허용되지 않는다는 대목에서 '아하~!' 소리가 절로 나왔다.
뭐, 문자열도 필요한 경우엔 mutable array 형태로 내부 조작을 할 수도 있다.

파이썬으로 윈도우 API도 호출하고 온갖 희한한 라이브러리를 동원해서 각종 컴퓨터 자동화 작업을 수행하고 별 걸 다 하는 친구도 있는데, 본인은 그 정도 수준은 안 된다. 그래도 이 정도만으로도 좋은 경험이다.

내게 파이썬을 권하던 후배 녀석이 이제는 HTML 공부도 좀 하라고 권한다. 이제는 플래시나 ActiveX 없이도 웹 표준 자체만으로도 별 걸 다 만드니, 훅킹을 한다거나 컴퓨터의 임의의 파일이나 레지스트리를 건드려야 하지 않는 이상 ActiveX의 필요성은 갈수록 없어지고 있다. 웹이 처음에는 그림+글+하이퍼텍스트로 된 문서일 뿐이었는데 지금은 그 자체가 거의 플랫폼처럼 됐다.

Posted by 사무엘

2011/05/25 08:18 2011/05/25 08:18
,
Response
No Trackback , 9 Comments
RSS :
http://moogi.new21.org/tc/rss/response/516

« Previous : 1 : 2 : 3 : 4 : 5 : 6 : 7 : Next »

블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2020/07   »
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  

Site Stats

Total hits:
1404137
Today:
27
Yesterday:
504