« Previous : 1 : 2 : 3 : 4 : 5 : ... 31 : Next »

디지털 컴퓨터가 취급하는 데이터라는 건 (1) machine word 하나에 다 들어가고 함수 인자에 값이 그대로 전해지는 primitive type이 아니면.. (2) 별도의 메모리를 할당해서 저장하고, 평소엔 그 메모리 주소를 가리키는 포인터만 대신 취급하는 complex type 이렇게 둘로 나뉜다.
그럼 primitive type은? (1) 정수, (2) 포인터, (3) 아니면 부동소수점으로 종류가 크게 나뉘는 것 같다.

문자열은 complex type이고, 문자 하나는 정수라는 primitive type에 속한다.
포인터는 물리적인 형태는 정수와 다를 바 없지만 그 숫자값의 성격, 의미와 용도가 여느 정수와는 전혀 다르다. 그리고 특정 프로그래밍 언어 이념이나 프로그래머의 편의를 구현하기 위해, 무슨 오프셋이나 카운터 같은 부가 정보를 곁들인 약간 뚱뚱한 포인터도 있다. (스마트 포인터, 자기 함수나 클래스의 바깥 문맥도 지원하는 포인터, 다중 상속을 지원하는 멤버 함수 포인터 등등~)

다음으로 부동소수점이 있다. 얘 역시 완전 별개의 영역이다. 얘는 잘 알다시피 과학 시간에 배우는 x.xxxx * 10^yy 이러는 숫자 표기법을 2진법 기반으로 컴퓨터에다 구현한 것이다. x를 mantissa, y를 exponent라고 한다.
얘는 딱딱 떨어지는 이산적인 정보를 좋아하는 컴퓨터에다가 현실의 연속적인(실무 또는 수학 계산) 계산값을 표현하려 애쓴 근성의 산물이다.

부동소수점은 자리수와 관계 없이 유효숫자가 일정하게 보장된다. 그렇기 때문에 -1 ~ 1 사이의 0.xxx 구간이 압도적으로 제일 정밀하다. 32비트건 64비트건, 부동소수점으로 표현 가능한 수의 무려 절반이 -1과 1 사이에 치우쳐 있다. 절대값 1 이상인 양수 지수부와, 그렇지 않은 음수 지수부가 반반씩이니까 말이다~!!

그리고 그 안에서도 0과 0.5 사이에 표현 가능한 수와 0.5와 1 사이에 표현 가능한 수는..?? 지수부의 크기에 비례해서 수백 배 이상 폭발적으로 차이가 난다. 이게 부동소수점의 심오한 세계이다. =_=;;
숫자가 커질수록 표현 가능 구간이 급격히 듬성듬성해지니.. 가장 흔히 쓰이는 축에 드는 32비트 single 부동소수점 기준으로 숫자가 1700만 정도로 커지고 나면 정밀도가 1이 되어 정수와 다를 바 없어진다.

또한, 1/2^n 형태가 아닌 모든 소수점은 원래 형태 그대로 정확하게 표현되지 못하고 유효숫자 이후의 뒷부분이 버려진다. 이 점 역시 감안해야 한다.
부동소수점 숫자를 하나 받아서 이 수의 바로 다음 크기인 수를 구하는 알고리즘을 구현해 보면 어떨까 싶다..;;

이 외에도.. x86에서는 정수끼리 나눗셈을 시키면 몫과 나머지가 같이 구해져서 레지스터에 저장된다. 그리고 0으로 나누는 건 CPU 차원에서의 오류/예외로 처리된다.
그러나 부동소수점에서의 나눗셈은 나머지라는 개념이 없다. 그리고 0으로 나눈 결과는 그냥 NaN이라는 값으로 처리된다. 이런 식으로 서로 관점과 동작이 차이가 있다.

초기화되지 않은 부동소수점 변수는 프로그래밍 언어 차원에서 NaN으로 초기화하는 게 한 가지 방법일 것 같다. NaN이 '쓰레기값' 역할을 수행하는 셈인데.. 내 기억으로 D 언어가 이걸 실제로 수행한다고 한다.
그리고 IEEE754 부동소수점 규격을 보면 NaN도 아직 에러까지는 아닌 quiet NaN, 그리고 에러인 signalling NaN으로 나뉘어 있다.

현실의 프로그래밍 언어에서는 IEEE32 (single, float) 내지 64 (double) 이 둘만을 제일 많이 볼 것이다. 당장 마소 Excel이 취급하는 숫자의 자료형만 해도 64비트 double이다.
그러나 사실은 표준 규격으로나 역사적으로는 이보다 더 다양한 부동소수점 규격이 존재한다.

  mantissa exponent
IEEE16 11 5
IEEE32 24 8
IEEE64 53 11
IEEE128 113 15
IEEE256 237 19
MBF32 24 8
MBF64 56 8
Turbo Pascal Real 40 8
long double (IEEE80) 65 15


같은 공간 안에서 유효숫자 개수와 표현 가능한 자리수 구획을 정하는 건 꽤 미묘한 고민거리인 것 같다. 한 가지 확실한 건 전체 공간이 커지더라도 exponent는 그에 비례해서 쭉쭉 커질 필요가 없으며, 로그함수 급으로 아주 느리게 증가해도 된다는 것이다. (비율이 갈수록 작아짐) 말 그대로 2의 exponent 승만큼의 자리수를 표현할 수 있기 때문이다.
exponent가 8만 돼도 0이 38개나 붙은 자리수를 표현할 수 있고, 바이트 경계가 딱 나뉘어서 처리하기 편하다. 그렇기 때문에 어지간한 부동소수점 규격들이 얘를 8비트로 잡은 걸 볼 수 있다.

MBF는 오늘날 같은 IEEE754 표준 규격이란 게 등장하기 전, 1980년대 마소의 BASIC 언어에서만 독자적으로 쓰였던 규격이다. 빌 게이츠와 폴 앨런이 젊은 시절에 나름 이런 것까지 독자적으로 만들어서 구현했다니..
MBF32는 IEEE32와 공간 크기와 분배 배율이 동일하지만, 비트 배치 순서가 다르다. 그렇기 때문에 서로 바이너리 차원에서 곧바로 호환되지는 않는다.

mantissa와 exponent 모두 내부적으로 부호 비트가 존재한다. 전자의 부호는 표현하는 숫자 자체의 양-음 여부를 결정하며, 후자의 부호는 숫자가 1보다 큰지 여부를 결정하게 된다.
저 표에서 mantissa-1을 3.3으로 나누면 (3.3의 의미는 ln(10)/ln(2)의 근사값) 10진법 기준의 유효숫자 개수가 나온다.
그리고 표현 가능한 범위도 exponent-1을 3.3으로 나누면 10진법 기준의 표현 가능 최대 자리수가 나온다.

맨 위의 16비트 부동소수점은 half-precison floating point라고 불리는데, 유효숫자가 3개밖에 안 되고 5비트짜리 exponent로는 최대 자리수도 겨우 10000대밖에 안 된다. 그러니 실용적인 가치는 매우 낮지만 이런 숫자도 머신러닝 계산용으로는 쓰이는가 보다. 그렇기 때문에 FP16이라는 옵션도 있는 거겠지?
그리고 볼랜드의 16비트 파스칼 컴파일러에만 전무후무 존재했던 6바이트 Real은 존재가 참 독보적이다. 4도 8도 아닌 그 중간.;;

부동소수점은 그 구조상 숫자 2개를 조합해서 한 숫자를 표현하니, 각종 산술 연산이나 비교 따위가 정수를 취급하는 것보다 무겁고 부담스럽다. 특히 자칫 잘못하면 동일한 숫자를 표현하는 방식이 여러 개 존재할 수 있게 되니, 이를 방지하기 위해서 자리수를 일정하게 맞추는 '정규화'라는 규칙이 필요하다.
그리고 부동소수점 연산은 초딩 시절에 배웠던 어림셈과 비슷한 면모가 있다. 아주 큰 수에다가 아주 작은 수를 많이 더하면 오차가 쌓이고 결과가 안 좋아진다.

쌍팔년도 시절엔 부동소수점 연산을 하드웨어 가속으로 보조해 주는 CPU 애드온이 별도로 존재했다. 일명 FPU, 코프로세서.. 그 시절엔 이거 하나만으로도 존재감과 가격이 지금으로 치면 고급 게임용 GPU나 마찬가지였다.

286~486 시절엔 모든 컴퓨터에 코프로세서가 있는 게 아니었다(486은 제일 저가 깡통 모델인 SX만). 그렇기 때문에 그 시절의 컴파일러들은 부동소수점의 처리 방식을 지정하는 옵션이 있었다. 무슨 x87을 지원할지, 그런 FPU 코프로세서가 없는 경우를 대비한 소프트웨어 연산 처리 코드를 넣을지를 말이다. =_=;;

자고로 컴퓨터 프로그램이라면 정수나 포인터를 어떤 형태로든 취급하지 않고 동작한다는 건 거의 불가능하다.
그러나 부동소수점을 전혀 취급하지 않는 프로그램은 분야에 따라서는 얼마든지 있을 수 있다. 그러니 부동소수점을 더 빠르게 다루는 건 소프트웨어로나 하드웨어로나 오랫동안 추가 옵션으로 간주되었던 것이다.

하드웨어 현질 없이 소수점 연산을 빠르게 하기 위해서 고정소수점이라는 편법도 쓰였다. 기존 정수에다가 자리수만 기계적으로 옮기고 곱셈과 나눗셈 결과를 보정하는 것 말이다. 32비트 정수를 16:16 내지 26:6 이런 식으로 분할했다. 단점과 한계가 명백하지만 이게 성능 하나는 워낙 탁월하니.. 옛날 게임이나 폰트 엔진 같은 일부 분야에서 제한적으로 쓰였다. ㄲㄲㄲㄲㄲ

그러다가 펜티엄이 돼서야 부동소수점 명령이 CPU에 기본 내장되고 지원되게 됐다. 그랬는데 그 펜티엄에서 바로 FDIV 나눗셈 결함이 발견되기는 했지만.. 가정용 컴에서까지 걱정해야 할 무슨 심각한 보안 문제 급은 아니었다. 아주 극단적으로 크거나 작은 수를 다룰 때 아주 미세하게 발생하는 문제이기 때문에.

80비트 long double의 경우, x87 프로세서에서도 지원 자체는 한다. 심지어 더 작은 32/64비트 부동소수점을 다룰 때도 중간 계산 결과는 다 80비트로 취급하기도 한다. 그러나 x87 이후에 도입된 SIMD 명령은 80비트 부동소수점을 지원하지 않기 때문에 80비트가 사실상 봉인돼 버렸다.

이거 무슨 분당선 전철이 훗날 8량 편성으로 고정되면서 처음에 미리 만들어졌던 수서-오리의 10량 기준 승강장의 일부 영역이 봉인된 것과 비슷한 것과 비슷한 느낌이다.;; ㅋㅋㅋㅋㅋ
하물며 128이나 256비트짜리 초대형 부동소수점은 어디 쓰이는 곳이 있기는 한지 잘 모르겠다.

본인이 과거에 만들었던 프로그램 중에 부동소수점 연산을 많이 하는 축에 드는 놈으로는 "3차원 그래픽 시연 프로그램"이 있다. 빌드된 실행 파일을 들여다보면 x87 명령이 많이 쓰인 게 눈에 띄었다.
그런데 얘를 컴파일러를 업글해서 다시 빌드하니 코드의 레이아웃이 싹 바뀌었다. x87의 구닥다리 fmul fld fadd fstp 대신, addsd movaps mulsd 처럼 SIMD 명령이 쓰인 것이다.

사용자 삽입 이미지

얘는 부동소수점 한둘의 연산을 넘어, 벡터· 행렬 같은 여러 데이터의 연산을 한 명령으로 한꺼번에 처리해 주는 확장 명령이다. 1999년, 펜티엄 III에서 도입됐다.
이미 Visual C++ 200x 시절부터 이 명령을 사용해서 컴파일하는 옵션이 /arch에 딸려 있긴 했다. 그러다가 2012부터는 별다른 옵션이 없으면 이 명령 세트를 사용하는 게 디폴트가 됐다~!!

이게 예전 198~90년대에 x87 명령 사용 여부와 비슷한 컴파일 옵션인 셈이다. 2012에서는 Windows XP 지원도 공식적으로는 최초로 끊겼는데 참 많은 변화가 있었다.

이상이다. 부동소수점과 관련하여 할 말한 얘기가 생각보다 많았다. ^^
x87에는 사칙연산뿐만 아니라 제곱근, 삼각함수, 2를 밑으로 하는 지수와 로그 같은 간단한 초월함수까지 CPU 명령 하나로 해치워 준다. 그러나 그렇다고 모든 수학 함수를 지원하는 건 아니어서 e를 밑으로 하는 지수와 로그는 지원하지 않는다. 2는 지원하고 e는 지원하지 않는다니.. 진짜로 수학 대신 컴퓨터 지향적인듯. ㅎㅎ

그러니 CPU빨이 없는 수학 함수는 C 라이브러리에서 어떻게 구현돼 있을까..?? 궁금해진다.
그리고 부동소수점을 10진법 문자열로 변환하거나 vice versa하는 것 말이다. 이거 은근히 어렵고 번거로울 텐데? exponent와 mantissa를 다 진법 변환하면서 두벌일을 해야 하니까..
에니악 같은 초창기 컴퓨터가 그 비효율 삽질에도 불구하고 숫자를 처음부터 10진법 단위로 묶어서 표현한 이유도 이와 무관하지 않았지 싶다.

여담: 숫자 자체를 컴퓨터가 primitive로 지원하는 숫자 unit들 여러 개를 묶어서 complex type처럼 취급하는 분야는 다음과 같다.

  • 수십~수백 자리 어마어마하게 큰 정수: 공개(비대칭) 키 암호화 라이브러리에서 필요하다. 금융 거래 같은 데서..;; 얘만 기막히게 빠르게 처리해 주는 정수 연산 라이브러리도 있다.
  • 유리수: 부동소수점 단독으로는 유리수 하나도 정확하게 표현이 안 되니 정수 2개 분자/분모를 따로 취급한다. Windows 계산기가 내부적으로 이렇게 동작한다고 알려져 있다.
  • 복소수: 부동소수점 2개를 묶어서 실수/허수를 표현한다. 수학· 과학 일부 분야에서 쓰인다. C++에 complex라는 클래스가 있는데, 템플릿 형태여서 정수만으로 구성된 복소수도 만들 수는 있다.
  • 소수점만 임의의 자리수로: 전용 수학 패키지에서 쓰인다.
  • 행렬· 벡터, 사원수: 더 이상의 자세한 설명은 생략한다. 게임을 포함해 컴퓨터그래픽 분야에서 쓰인다.

Posted by 사무엘

2024/04/25 08:35 2024/04/25 08:35
, ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2290

1. Windows의 컴퓨터 비트 수 변화

과거에 주류 PC 환경이 (1) 16비트에서 32비트로 바뀌면서 소프트웨어 개발 환경이 크게 바뀌었다.
int와 WPARAM, handle, 포인터가 모두 4바이트 크기로 바뀌었고, 이로 인해 메시지도 몇몇은 스펙이 불가피하게 바뀌었다.
좌표계의 기본 단위도 다들 32비트로 확장됐고, 이로 인해 GDI 함수들이 상당수가 Ex 버전으로 바뀌었다. 왜냐하면 예전처럼 x, y 좌표 둘을 long 하나에다 묶어서 전달할 수가 없어졌기 때문이다.

하지만 선점형 멀티스레드가 지원되고 그 전에 모든 프로세스들이 자기만의 독립된 주소 공간을 갖는다는 건.. 과거엔 정말 상상도 못 할 혜택이다.
8비트야 거의 임베디드 급의 열악한 환경이니 멀티태스킹 따위는 별나라 얘기였다. 16비트 시절엔.. 어정쩡하게 아주 불편하고 힘들게 가능했던 반면.. 32비트가 되니 주소 공간도 넉넉하고 이제 좀 그럭저럭 할 만해진 것이다.

그리고 32비트에 와서는 예전에 깐깐하게 구분해야 했던 게 이제는 구분이 필요 없어지고(예: HINSTANCE vs HMODULE, far vs near), 예전에는 꼭 할당하고 해제해 줘야 했던 게 지금은 그럴 필요가 없는 등(resource 관련 API, MAKEPROC 따위).. 프로그래밍 하기가 전반적으로 더 간편해지고 편리해지기도 했다.

그에 비해 (2) 32비트에서 64비트로의 변화는 뭐.. int와 포인터의 크기가 달라진 것으로 인한 자잘한 충돌과 이식성 문제가 고작이다. 4GB 한계가 없어지기만 했을 뿐, 체감되는 변화는 아주 미미하다.
Windows의 경우, int는 물론 long조차도 여전히 32비트 크기로 유지된다. 그러나 WPARAM은 64비트로 확장됐다.

전에도 한번 얘기했듯이 게임기는 1990년대 후반, PC는 2000년대 후반, 스마트폰은 2010년대 후반이 돼서야 슬슬 64비트 시대에 들어섰다.
이런 곳은 비트 수가 점진적으로 늘어났기 때문에 기존 코드와의 호환성이 중요했다. 그렇기 때문에 포인터만 빼고 int나 long은 4바이트로 할지 8바이트로 할지 고민이 많은 편이었다.

그 반면.. 슈퍼컴퓨터 전용 아키텍처가 있던 시절 말이다. 197, 80년대에 처음부터 64비트로 시작했던 컴터 환경에서는 레거시 고민 따위 없었다. Cray 같은 플랫폼에서는 쿨하게 처음부터 int고 포인터고 몽땅 다 무식하게 64비트 모델을 채용한 곳도 있었다고 한다. 물론 오늘날이야 int까지 8바이트인 컴퓨팅 환경은 없다고 봐도 되지만..
그리고 저런 옛날 컴퓨터들은 데이터를 취급하고 연산하는 단위만 64비트였다. 아무리 슈퍼컴이라 해도 자기네 메모리 용량이 4GB에 미치지는 못했기 때문에 64비트 컴퓨팅이 곧 64비트 addressing을 의미하지는 않았다고 한다. addressing까지 다 되는 64비트 CPU는 1990년대가 돼서야 등장했다. (MIPS, DEC Alpha 따위) 아하~

얘기가 좀 옆길로 샜는데.. 아무튼 Windows는 16비트에서 32비트로 넘어갈 때 변화가 좀 있었고, 32에서 64비트로의 변화는 미미한 편이었다. 그럼 Windows의 역사상 16비트에서 32비트로의 전환만이 대격변이었던 것일까?
꼭 그렇지는 않았다. 오히려 더 옛날, (3) Windows 1 (+2)과 3 사이는 플랫폼 SDK의 변화, C 컴파일러의 변화 등의 단절이 더 심했다.

Windows 1과 2는 아직도 리얼 모드 내지 끽해야 286 표준 모드에서 멀티태스킹을 구현하던 정말 암울한 시절이다.
Windows의 오랜 역사를 좀 아는 guru라면, 20세기에 Windows에서 가장 혁신적인 변화는 바로 95나 NT도 아니고 3.0에서 "386 확장(enhanced) 모드"가 정식 도입되었던 사건이라고 말할 정도이다. (☞ 링크)

그랬기 때문에 Windows 1과 3은 같은 16비트 기계어에 같은 NE 포맷임에도 불구하고 1용 프로그램이 후대의 3 내지 9x에서 제대로 실행되지 않을 가능성이 매우 높았다.
게다가 저 1980년대의 구닥다리 C 컴파일러는 함수를 정의하는 문법조차 ANSI가 아닌 기괴한 K&R 방식이었다니.. 소스 레벨의 호환성도 기대하기 어렵겠다. 더 자세한 건 여기 글을 참고하시라. (☞ 링크)

2. 마소의 16비트 P-code 기술

마소와 관련된 옛날 이야기가 계속 이어진다. 이 블로그에서 본인이 지금까지 이 얘기를 한 번도 꺼낸 적이 없었다니 놀랍다.;;
네이티브 기계어가 아니라 다른 중립적인 바이트코드 기반으로 돌아가는 '가상 기계 프로그램'이라 하면 흔히 Java (JVM)나 C# (.NET, CLR) 같은 것만 떠올리기 쉽다. 이런 건 최소 32비트 이상의 컴퓨팅 환경에서 등장한 런타임 환경이다. 고유한 클래스 라이브러리도 갖고 있고 쓰레기 수집기도 제공한다.

하지만 마소는 창립하자마자 그 허접한 197, 80년대 8비트 컴퓨터로 제일 먼저 만들었던 게 BASIC 인터프리터였다. 현대적인 가상 머신 정도로 거창하지는 않지만, 그래도 고유한 바이트코드 가상머신 기술을 보유해서 16비트 컴퓨팅 시대까지 잘 써먹었다.

마소에서는 그 바이트코드를 스스로 P-code라고 불렀다. P는 pseudo-, portable, packed(조밀) 등을 뜻했다고 한다. 그리고 그걸 Basic뿐만 아니라 C/C++ 언어 컴파일러에다가도 접목했었다. 아니, 베이식은 그렇다 치지만 기계어 직통 컴파일이 당연시되는 언어이던 C/C++에다가는 성능(= 실행 속도) 희생까지 감수하면서 도대체 왜..?

이 바이트코드는 크기가 작았기 때문이다. 이게 packed의 의미이다.
같은 프로그램 소스를 비슷한 최적화 수준으로 컴파일 했을 때, 네이티브 x86 기계어 코드보다 훨씬 더 작은 크기로 표현할 수 있었다. 심지어 P-code를 해독하는 가상머신 코드의 오버헤드(9K 남짓?)를 포함시키더라도 수지맞는 장사였을 정도라니.. 이건 뭐 실행 파일 압축 기능까지 약간이나마 겸한 셈이었다.

컴퓨터 역사의 관점에서 볼 때 x86 자체도 골수 CISC 구조로서, 현대적인 아키텍처 대비 기계어 코드가 조밀하고 크기가 아주 작은 축에 드는 아키텍처라고 여겨진다. (그 대신 읽어들이고 디코딩하는 난이도가 쥐약이고, 저전력 모바일과 상극)
그런데 마소의 P-code는 그 악명 높던 x86 기계어보다도 더 조밀하고 작다니.. 그 시절에 얼마나 메모리가 비싸고 귀했고 메모리를 어떻게든 아껴야 했는지가 실감이 간다. PC에서도 386 486 같은 32비트 CPU는 진작에 등장하고 값도 내려갔지만.. 메모리가 아직 병목이었다. 이게 더 싸지고 풍부해진 뒤에야 본격적으로 Windows 95/NT가 쓰일 수 있었다.

Visual Basic이야 exe를 생성한다 해도 런타임 dll이 따로 필요하고 내부 코드는 P-code 기반이었다. 1997년에 출시된 5.0.. 최초로 32비트 전용으로 출시된 이 버전에 이르러서야 네이티브 코드 컴파일 기능이 도입됐다.
C/C++의 경우, MS C/C++ 7.0과 Visual C++ 1.x 시절.. 16비트 한정으로 이런 기능이 있다가 32비트부터는 폐기됐다. 그 대신, 16비트이기만 하면 플랫폼은 DOS와 Windows를 모두 지원했다.

따지고 보면 Windows NT의 32비트 PE (portable executable)는 저런 P-code와는 접점이 없었던 셈이다. 32비트 Visual Basic 5나 6을 쓰지 않는 한 말이다
자세한 것은 이 링크의 설명을 참고하시라. 마소의 전설적인 P-code에 대해서 구체적으로 소개한 글은 "Microsoft P-Code Technology" by Andy Padawer이 유일한 것 같다.

QuickBasic이나 GWBASIC은 소스 코드를 고유한 바이너리 포맷으로 저장하는 기능이 있었다. 이건 세상 그 어느 프로그램 개발 환경에서도 없는 기능이었지 싶다.
그 반면, 저 P-code는 소스 코드가 아니라 나름 기계어를 표방하고 컴파일된 코드였다는 차이가 있다.

3. 마소와 볼랜드 프로그래밍 툴의 Windows 지원 내력

(1) 아마 예전에 이 얘기를 한 적이 있었을 텐데..
1980년대 말부터 마소와 볼랜드에서는 주요 프로그래밍/개발툴을 내놓으면서 뭔가 교육용 저가 보급형 제품군에다가는 각각 Quick과 Turbo라는 스피디한 브랜드명을 붙였고, 기업용 기함급 모델에다가는 그냥 자기 회사 이름을 붙였었다.

(2) 1990년대 초엔 C 컴파일러에는 C++의 지원이 추가되었다. 그래서 지원 언어 표기가 C/C++이라고 바뀌었다.
마소의 경우, QuickC는 Microsoft C를 먼저 만들다가 곁다리로 병행하며 잠깐 만들었던 제품이다. 이건 C++ 지원 없이 겨우 2.0에서 맥이 끊겼다. 그 대신 이전부터 만들어 오던 MS C 6의 다음 버전이 MS C/C++ 7이 되었다(1992). 그리고 이거 다음 버전부터는 그 이름도 찬란한 Visual 브랜드가 시작됐고, C는 떼어낸 채 Visual C++ 1로 넘어갔다.

저 때는 1993년 무렵이었다. Visual C++은 Windows NT와도 역사를 함께한다. 이게 마소에서 최초로 내놓은 32비트 C/C++ 컴파일러이며, Windows NT 내부의 각종 프로그램들을 빌드하는 용도로, 즉 자체적으로도 쓰였기 때문이다.
물론 Visual C++도 1.5까지는 16비트 버전이 같이 나오긴 했었다. 그리고 대외적인 버전 번호는 1로 리셋됐지만 얘 역시 MS C를 계승한 제품이라는 흔적은 MSC_VER이던가 그 매크로 상수의 번호에 남아 있다.

(3) 한편, 볼랜드 진영에서는 Turbo C 2.0의 다음 작품이 Turbo C++ 1.0이 되었다. 제품명과 버전이 다 리셋됐다니 좀 이례적이다.
그리고 그 다음 버전인 Turbo C++ 2때부터 같은 버전의 Borland C++도 나란히 나오기 시작했다고는 하는데.. 실질적으로 Turbo와 Borland의 구분이 생겼다고 일반인들이 존재감을 인지하는 첫 버전은 3이다.
Turbo C++은 3인가 3.1에서 맥이 끊겼다. 그 뒤 적어도 4~5 버전부터는 Borland C++만 나오다가 RAD 툴인 C++ Builder 1로 넘어갔다.

(4) 그 시절 C/C++ 컴파일러 업계에서는 C++ 지원뿐만 아니라 Windows 플랫폼의 지원도 중요한 이슈였다.
마소는 DOS에 이어 Windows를 만들던 본가였고, C는 어셈블리어와 더불어 자기들 제품을 만들 때 사용되는 주력 언어이기도 했다. 그러니 MS C는 처음부터 Windows를 지원하는 게 너무 당연한 일이었다.

1980년대 중반, 정말 구닥다리 MS C 4~5 시절부터.. 그야말로 전설적인 Windows 1.x, 2.x 프로그램을 만들 수 있었다. 단, QuickC는 DOS용 버전 2.x대와 별개로 QuickC for Windows 1.0이 딱 한 번만 나오고 말았던 듯하다. 요컨대 마소는 QuickC의 Windows 버전만이 버전 리셋을 했고, 볼랜드는 C++ 컴파일러를 구현할 때 버전 리셋을 했다.

그에 비해 볼랜드 제품에서 Windows 지원이 추가된 건 버전 3.x부터로, C++까지 지원되고 난 이후의 일이다. 심지어 Win32의 지원은 Windows 95가 출시되고 4.x 정도는 된 뒤부터다. 후발주자 3rd-party 업체이니 이런 것 수용은 한 발 늦을 수 있다지만.. Windows용 32비트 extender까지 미리 만들었던 Watcom 같은 업체하고는 개발 방향이 많이 달랐던 것 같다. 그 대신 볼랜드에서는 OWL이라고 꽤 잘 만든 객체지향 프레임워크를 연구 개발했다.
이렇듯, Windows 지원과 관련해서는 볼랜드와 마소 개발툴 간에 이런 내력의 차이가 있었던 셈이다.

(5) 자, 그럼 C/C++ 다음으로 파스칼의 세계로 가면..
볼랜드에서 Turbo Pascal을 내놓으면서 1980년대를 호령하고 재미를 봤다. 도스 아니면 기껏해야 OS/2에서 말이다. 그러다가 1990년대 초, Turbo Pascal 6 타이밍 때 TP for Windows를 1.0과 1.5 두 차례 내놓았다. 아마 5.x던가 6이던가..
이때 ObjectPascal이라는 객체지향 문법이 언어에 도입되기도 했지만 이건 TP의 버전에 영향을 주지 않았다. 그 대신 Windows용을 1.0부터 다시 내놓았다는 점이 Turbo C++과는 다르다.

그러다가 Turbo Pascal 버전 7이 Borland Pascal 7과 나란히 출시됐으며.. 이 BP7은 TP for Windows 2를 통합· 포함한 형태가 됐다. 제품 라인업 한번 복잡하네..;;
TPW는 about 대화상자에 수학자 파스칼 얼굴이 그려져 있는 반면, BPW는 그렇지 않다는 차이가 있다.;;;

사용자 삽입 이미지사용자 삽입 이미지

1992년에 출시된 Borland C++ 3.1, 그리고 Borland Pascal 7이 도스와 Win16을 풍미했던 장수만세 안정판으로 여겨진다.
Borland C++은 C++ Builder로 넘어가기 전 1993~1995년 사이에 자체적으로 버전이 4~5까지 올라가기도 한 반면, BP는 델파이로 넘어가기 전에 딱히 버전업이 없었다.
심지어 Delphi도 1995년의 첫 버전 1은 Win16, 16비트용이었고 버전 2부터 Win32로 넘어갔으니, 32비트화도 C++보다 늦은 셈이다.

한편, 마소는?? 처음에 Microsoft Pascal을 1980년대에 4.x 버전까지 개발했었다. 하지만 이건 Turbo Pascal과의 경쟁에서 승산이 없다고 판단했는지 접었다. 그렇게 접기 직전에 경쟁사 제품처럼 뽀대나는 IDE를 얹은 QuickPascal 1.0을 최후의 발악 차원에서 한번 내놓았을 뿐이다. Windows 지원 같은 것도 당연히 없었고 제품의 맥이 끊겼다.
볼랜드에서는 Turbo Basic을 만들었다가 반대로 마소의 QuickBasic 대비 승산이 없다고 생각해서 포기해 버렸으니.. 행보가 서로 정반대인 셈이다.

Posted by 사무엘

2024/03/30 08:35 2024/03/30 08:35
, , ,
Response
No Trackback , 2 Comments
RSS :
http://moogi.new21.org/tc/rss/response/2281

1. 자동차 배터리와 컴퓨터 스택 메모리

자동차의 배터리 방전과 컴퓨터 프로그램의 스택 오버플로는 서로 완전히 다른 분야이긴 하지만 공돌이의 심상 면에서 공통점이 느껴지는 것도 좀 있는 것 같다.
지난 수십 년 동안 자동차는 엔진 효율이 크게 향상됐고 컴퓨터의 메모리도 동적 힙(heap)이야 넘사벽 급으로 용량이 뻥튀기 됐지만, 저것들은 용량이 획기적으로 크게 올라간 적이 없다. (특별히 그럴 필요가 없어서..)

그리고 저 둘은 남은 용량이나 고갈 징후를 미리 알려 주는 메커니즘도 딱히 존재하지 않는다.
자동차의 황산-납 배터리는 스마트폰이나 노트북의 리튬이온 배터리처럼 용량이 몇 % 남았다고 알려주는 기능이 없다. 블랙박스도 배터리의 전력 용량이 감소하면서 전압도 같이 감소하는 걸 감지해서 간접적으로 꺼진다거나 할 뿐이다.

컴퓨터 프로그램의 스택 메모리도 지역변수의 주소를 이용해서 남은 용량을 아주 간접적으로 유추할 수 있고, 시스템 exception을 이용해서 일이 벌어졌을 때 스택 overflow를 감지할 수도 있다. 하지만 이식성 있는 일반적인 방법은 존재하지 않는다. 그걸 일일이 체크하는 건 아주 비효율적이다. 이런 식으로 유사점이 있다.

2. TLS 슬롯

메모리와 관련하여 이렇게 아기자기하게 제약이 될 수 있는 게 본인이 보기엔 TLS 슬롯 공간이다.
사실, 한 사람이 만드는 프로그램이 수십~수백 종류의 코드가 동시에 실행되는 프로그램을 만들 일은 극히 드물겠지만, 문제는 내가 만들지 않은 프로그램(특히 DLL)들이 한 프로세스에 왕창 붙어서 실행될 때이다. 이런 코드들이 전부 TLS 슬롯을 하나씩만 요청하더라도 TLS 공간은 수십~수백 개씩 소모될 수 있다.

Windows 95 내지 NT 3.x시절에는 현실적으로 필요한 양과 그 당시 컴퓨터들의 평균적인 사양을 감안해서 스레드마다 TLS 슬롯이 64개씩 배당되었다. 이게 바로 TLS_MINIMUM_AVAILABLE라는 상수값의 의미이다. 역대 win32 환경 중에 TLS 슬롯이 제일 적었던 구현체가 제공했던 최소 개수가 64라는 뜻이다.

세월이 흐를수록 기본 제공되는 TLS 슬롯 수는 점차 늘어나서 Windows XP/2000부터는 64에다가 1024가 더해진 1088개라고 한다. 후대의 운영체제에서 슬롯 수가 이것보다 더 늘어났다는 얘기는 본인은 들어 보지 못했다. 힙 메모리처럼 엿가락처럼 한없이 더 늘어나야 할 필요는 없는 물건이기 때문이다.

스레드 단위로 전역적으로 공유돼야 하는 데이터가 많이 있거나 그 데이터의 길이가 들쭉날쭉 변한다면 별도의 heap 메모리를 할당하고 TLS 슬롯에다가는 포인터만 저장해야 한다. 즉, 응용 프로그램은 언제나 고정된 개수만의 슬롯을 사용하고, 슬롯 사용량을 최소화해야 한다.

내가 보기에 TLS가 꼭 필요한 때 중 하나는 사용하는 라이브러리가 너무 구닥다리여서 콜백 함수에 context 데이터를 넘겨주는 게 없어서 context 정보를 오로지 전역변수에 의지해야 할 때(+ 그런데 thread-safety가 보장돼야 할 때) 정도이다.
한 프로그램이 스레드 10개로 동작하건 100개로 동작하건 그건 소모되는 TLS 슬롯 개수와는 전혀 무관하다. 이건 그냥 실행되는 코드의 종류하고만 관계가 있다.

3. 복합 스레드 동기화

조금 부끄러운 얘기를 하자면.. 본인은 오래 전 학교의 컴공 운영체제 시간에 졸았는지, 아니면 다른 변고가 있었는지는 모르겠지만 나만의 스레드 동기화 오브젝트를 새로 만든다는 개념 내지 필요성을 지금까지 별로 이해하지 못했다. 그냥 운영체제가 제공하는 critical section이나 뮤텍스, 세마포어만 쓰면 끝이지 않은가..?? 그랬는데 회사 코드를 들여다보면서 뒤늦게야 '아...!!' 현타 비스무리한 걸 경험하게 됐다.

평범한 데이터 컨테이너가 멀티스레드 동작에 대비하여 곳곳에 뮤텍스 기반의 lock이 걸려 있는데.. 데이터를 건드리는 set쪽뿐만 아니라 단순히 read만 하는 get 메소드들까지도 내부가 전부 동일한 lock이 일일이 걸려 있었다. 흠, 이건 read일 뿐인데 여러 스레드가 동시에 접근해도 상관없지 않나? 저건 불필요한 삽질 오버헤드 아닌가? 이 lock은 빼 버려도 되겠는데?

그런데 알고 보니 그 생각은 절반만 맞았다. reader만 있을 때는 여러 스레드들이 동시에 접근해도 괜찮지만, 한 스레드라도 write를 할 때는 다른 writer는 물론이고 reader들도 다 줄 서서 기다려야 하기 때문이다. 그러니 get 함수도 lock이 전혀 없어서는 안 된다.

하지만 이렇게 정말 간단하고 범용적인 원리대로 lock의 종류를 구분하는 것이 운영체제 API로는 의외로 직통 구현돼 있지 않았다. 기존 동기화 오브젝트를 조합해서 사용자가 직접 구현해도 되며, 이런 복합 동기화는 읽기/쓰기 중 한쪽으로만 CPU가 너무 쏠리고 있을 때 분배를 어떻게 할지 같은 정책이 일방적으로 획일화 가능하지 않기 때문이다.

그래도 C++ 라이브러리의 스레드/동기화 클래스 중에는 이런 "다수 reader/단일 writer" 동기화를 구현한 놈이 혹시 있는지 모르겠다. 이런 클래스는 그냥 lock와 unlock만 있는 게 아니라 lock이 lockForRead와 lockForWrite로 세분화된다.

이렇게 custom 동기화 오브젝트를 만들면 기존 운영체제 API만으로는 바로 구현 가능하지 않은 복합 동기화를 시전할 수 있다. reader와 writer가 여러 놈이 동시에 경합할 때, 그리고 read와 write의 소요 시간이 차이가 많이 날 때 어떤 원칙으로 CPU를 배분할지 같은 세부 원칙도 손수 정할 수 있다.

그리고 디버그 빌드에서는 각종 참고 정보를 알려주고 오류를 검출하는 기능도 넣을 수 있다. 현재 포커스를 잡은 스레드, 또는 대기 중인 스레드들을 얻어 온다거나..
lock을 건 스레드가 아직 실행 중인데 그 동기화 오브젝트가 소멸되는 건 잘못된 상황이므로 곧장 예외나 assertion failure를 날린다.

(이 글을 쓰고 나서 나중에 알게 된 사실: Windows Vista에서부터 Slim Reader/Writer (SRW) Lock이라는 게 도입됐다. 크리티컬 섹션처럼 동일 프로세스 안에서만 사용 가능한 대신.. reader lock과 writer lock을 구분해서 요청 가능한 작고 가벼운 동기화 오브젝트이다. 역시 범용성이 있으니 2000년대 이후에야 도입됐구나 싶다.)

그리고 디버깅에 제일 도움이 되는 정보는.. 아무래도 deadlock 감지일 것이다.
응답이 멎은 프로그램을 강제로 break시킨 뒤, 스레드들의 스택 상태를 추적하다 보면 memory leak보다는 쉽게 찾을 수 있겠지만 크고 복잡하고 스레드를 왕창 많이 만드는 프로그램에서 이런 걸 프로그램이 디버그 정보를 통해 자동으로 찾아주면 디버깅에 큰 도움이 될 것이다. 물론 이 경우, 동기화 오브젝트가 락에 진입한 스레드들의 실행 정보를 일일이 관리하고 있어야 할 것이다.

Posted by 사무엘

2024/01/24 08:35 2024/01/24 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2256

1. 플랫폼 공통 스크립트

(1) qt가 단일 소스로 Windows, 리눅스, 맥에서 모두 똑같이 돌아갈 수 있는 GUI 프레임워크라면..
cmake는 단일 스크립트로 Visual Studio 프로젝트, 유닉스 계열 makefile, 그리고 xcode 프로젝트를 모두 생성해 주는 메타빌드 시스템이다.
그렇다면 qt를 사용해서 프로그램을 개발하고, 프로젝트 파일을 cmake로 관리한다면 진정한 크로스플랫폼 프로젝트를 만들 수 있을 것 같다. ㄲㄲㄲ

(2) 다음으로, 단일 소스/스크립트 기반으로 세 운영체제에서 똑같이 돌아가는 설치· 배포 패키지 생성 유틸은 없는지 궁금하다.
cmake(메타빌드)와 nsis(설치· 배포)는 스크립트 언어가 완전히 같은 문법 기반은 아니지만 좀 비슷하고 공통 조상을 둔 게 있는 것 같다.
얘들은 전문적인 프로그래밍 언어가 아니기 때문에 복잡한 수식에 복잡한 객체 선언, 배배 꼬인 복잡한 조건 분기 반복을 구현할 수는 없다는 공통점이 있다. 그리고 변수는 $로 시작해서 선언하고, 문자열 리터럴 안에다가 변수값을 바로 집어넣을 수 있다는 것도 비슷하다.

시대에 좀 뒤떨어지는 설치 배포 패키지는 고해상도 DPI를 지원하지 않는 경우가 있어서 좀 안습하다. 125~150% 배율 화면에서 설치 프로그램부터가 강제 확대되는 바람에 창이 뿌옇게 표시되면.. 정작 프로그램 당사자가 고해상도 DPI를 지원한다 해도 그 프로그램의 첫 사용 경험이 좋게 시작될 수 없을 테니 말이다. 본인은 이런 사례를 몇 번 본 적이 있다.

2. C/C++ 컴파일러

Windows용으로 쓸 만한 좀 가벼운 C/C++ 컴파일러가 없는지 좀 궁금하다.

  • 용량은 그냥 수십 MB 수준이며, 단독으로는 그냥 표준 C/C++ 라이브러리만 들어있고 명령 프롬프트 프로그램만 만들 수 있다.
  • MFC 같은 건 없어도 되고, 그냥 따로 설치한 플랫폼 SDK와 연계하면 Windows API 정도는 사용할 수 있다.
  • 프로젝트 없이 간단한 소스 코드 하나만으로 exe를 바로 만들 수 있다.
  • 특히 Visual Studio Code와 바로 연계해서 쓸 수 있다.

Visual C++은 정말 너무 무거워졌고.. Windows용 g++인지 뭔지는 런타임인 cygwin 깔고 이것저것 선행 작업이 많이 필요해서 무겁긴 마찬가지이다.
이렇게 딱 본질에만 충실한 개발 환경을 어디 구할 데 없을까? 개발툴이 무거워지는 건 인스턴트 메신저 프로그램들이 수익성 컨텐츠 집어넣느라 쓸데없이 너무 무거워지는 것과 비슷해 보인다.

요즘은 웹에서 어지간한 프로그래밍 언어들을 바로 코딩하고 돌려볼 수는 있다. 하지만 웹에서의 코딩 환경은 로컬 IDE와 같은 급으로 인텔리센스 자동 완성이 지원되지는 못하니 생산성이 떨어진다.
옛~~날에 요런 틈새시장 용으로 Dev C/C++라는 물건이 있었던 걸로 기억한다. IDE와 컴파일러 복합이었고, 개발사의 이름에 blood라는 단어가 있었는데=_=.. 그 뒤로 개발이 중단된 듯하다.

3. Visual Basic

Visual Basic 6은 사법시험 같고, Visual Basic .NET은 로스쿨 같다는 생각이 든다. 이렇게만 말하면 무슨 뜻인지 아시려나..?? 내가 보기엔 딱 그렇다. -_-;;

참고로, Visual Studio 툴 자체를 설치하지 않더라도, 닷넷용 언어들의 커맨드라인 컴파일러는 .NET 프레임워크를 설치하면 깔린다.
그러니 Windows에서는 리눅스처럼 gcc g++은 없지만, Windows\Microsoft .NET\아무 버전.. 디렉터리 가 보면
vbc (비베), csc (C#) 컴파일러는 어느 컴에나 다 있다.
그런데 C/C++ 컴파일러는 없으니 아쉽다. 비베는.. 6이건 .NET이건 실무 용도가 있기는 한지 개인적으로 궁금하다.

4. git

이놈의 git은 그냥 commit이나 push를 하기 전에 미리 "지금 원격 저장소에는 또 최신 작업 내역이 있는데요. pull부터 먼저 하시겠습니까?" 이렇게 말이다.
커밋할 때부터 지금 중앙 저장소의 상태가 최신이 아니니까 미리 니 쪽에서 pull부터 하고 나서 커밋 하는게 좋겠다고 좀 알려줬으면 좋겠다.
맨날 push할 때 충돌 난다고 뒤늦게 징징대서 사람 귀찮게 하지 말고 말이다. 이러면 commit 그래프도 일직선이 아니라 더 지저분한 모양이 된다. 이건 시스템이 좀 개선돼야 할 것 같다.

5. Visual Studio Code

오~ 써 보니 사용자 경험이 좋고 꽤 괜찮다!!
빌드 가능할 정도로 정교하게 프로젝트/makefile을 세팅할 필요 없이 디렉터리만 지정해 주면, 거기 있는 소스와 헤더 파일을 알아서 '적당히' 파싱 해서 심벌과 파일명 검색, 명칭 자동 완성이 가능한 범언어적 에디터.
요런 틈새시장 제품이 Source Insight밖에 없는 줄 알았는데 말이다. 쟤도 그 틈새를 멋지게 잘 공략했다.

외형 껍데기가 깔끔 모던하고, 파일 내용 변경한 게 find in files 결과창 같은 데에 실시간으로 쓱쓱 반영되는 것도 좋다.
마구 마구 아이디어가 샘솟고 코딩을 하고 싶어진다.
걍 Visual Studio IDE만 쓰면 되지 에디터가 굳이 따로 필요하나 소신이었는데, 이 정도 에디터면 프로그래밍 생산성에도 긍정적인 영향을 줄 것 같다.
Source Insight는 유료인 반면, 쟤는 무료이기까지 하다. Source Insight 측에서 분발해야 할 듯.

6. 궁금한 것: 공유 라이브러리 디렉터리

Windows에는 프로그램의 빌드 때만 쓰이는 정적 라이브러리인 lib, 그리고 프로그램이 실행될 때 매번 쓰이는 동적 라이브러리 dll이 있다. dll을 찾는 순서로는 현 디렉터리, 실행 파일이 있는 디렉터리, Windows 시스템 디렉터리, PATH에 등록된 디렉터리 등.. 여러 복잡한 절차가 존재한다.

시스템 디렉터리의 포화를 막고 DLL hell 현상도 해소하려고 20년도 더 전에 side-by-side assembly라는 기법이 도입되긴 했다. 하지만 사용이 너무 까다로워서 그런지 이건 마소 자기들끼리만 쓰고 제3자 개발자들은 잘 쓰지 않는 것 같다.
그리고 COM이야.. 파일 이름이나 디렉터리 같은 저수준 방식이 아니라 객체의 클래스ID로 DLL을 식별하는 거나 마찬가지이다. 깔끔하기는 하지만 레지스트리를 건드려야 하고 다른 방식으로 사용이 너무 까다롭고 복잡하다.
이미 COM을 기반으로 만들어진 DirectX, OLE 같은 특정 분야의 API를 사용할 때나 이걸 쓰지, 얘 방식으로 뭔가 새로운 컴포넌트를 만드는 일은 잘 없다. =_=;;

자, Windows 동네는 상황이 이렇고, 유닉스 계열에서는 이와 비슷한 개념으로 정적 라이브러리 a와 동적 라이브러리 so가 있는 걸로 안다. 그리고 내가 알기로, 거기도 so를 특정 사용자용 bin, 공용 bin 등으로 구분해서 수용하며, so 파일을 찾는 정형화된 절차가 있다. 구체적인 내역은 모르지만 말이다.
macOS는 거기에다가 dylib인지 framework인지 하는 개념도 있다. 이건 Windows의 side-by-side assembly나 COM처럼 자신들만의 컴포넌트 규격인 걸까? 이것들의 관계는 무엇인지 잘 모르겠다.

개인적으로 C/C++을 처음 공부하던 시절에도 모듈과 번역 단위 개념이 나올 때부터 생소하고 어려웠다. 무엇이든 범위가 여러 소스 파일, 여러 파일 수준이 되면 어려워지는 것 같다.

7. 콘솔(터미널): 화면을 모두 지우는 명령 등

Windows의 명령 프롬프트에서 CLS는 그야말로 현재 콘솔 버퍼에 있는 모든 출력 내용들을 싹 다 날리는 명령이다. 명령 프롬프트의 강화 버전인 PowerShell이나 Windows Terminal에서도 동일하게 사용 가능하다.
그러나 맥과 putty 터미널에서 clear는 기존 표시된 내용들을 다 위쪽으로 밀어내서 지금 보이는 겉보기 화면만 싹 정리된 듯이 보이게 한다. 화면을 위로 스크롤 시키면 기존 내용들을 여전히 다 확인할 수 있다.

난 개인적으로 이 동작이 굉장히 성가시고 불편했다. 빌드를 돌리고 나서 에러를 확인한 뒤, 에러를 고치고 clear 후 다시 빌드를 돌리는데 이전 빌드의 에러가 자꾸 검색되면 좋을 게 없기 때문이다.
화면을 CLS처럼 완전히 싹 지우는 기능은 '스크롤백 날리기'라고 보통 메뉴에서 별도의 명령으로 존재하는 편이더라. 차라리 clear이나 CLS 명령의 옵션으로 둘 다(전체 vs 한 화면만) 제공하면 어떨까 싶지만, 또 그렇지는 않더라.

개인적인 생각은 다른 터미널들에서도 모든 출력을 싹 날리는 게 더 쉽게 가능했으면 좋겠다.
도스의 배치 파일에도 if errorlevel goto 같은 아주 간단한 제어문이 지원되긴 했지만, 유닉스 계열의 셸 스크립트는 말할 것도 없고 GWBASIC하고도 비교가 민망한 허접한 기능밖에 없었다.;;

탐색기에서 자기 컴퓨터뿐만 아니라 LAN/FTP 상의 다른 컴퓨터까지 바로 들어갈 수 있으면 좋다.
그것처럼 한 터미널에서 내 컴뿐만 아니라 원격 컴퓨터의 터미널에도 바로 들어갈 수 있으면 좋을 것 같다.
마소에서도 이에 대한 필요성을 느끼고 마냥 도스 기반이 아니라 더 전문화된 터미널 앱을 제공하는 것이지 싶다. 너무 늙은 putty조차 대체할 수 있게 말이다.

putty는 문자열 찾는 기능과 특정 문자열이 나타났을 때 highlight 표시하는 기능이 좀 있었으면 좋겠다.

8. 앱들의 개발 형태의 변화

어제 오늘 일은 당연히 아니지만.. 개인용 컴퓨터라는 게 인터넷 단말기나 게임기로 바뀌어 가니.. 단순 정보 조회 프로그램도 이제는 다 PC가 아니라 웹 기반으로 바뀌어 간다. 예전 같았으면 RAD 툴이라도 썼을 법한 프로그램도 이제는 어지간해서는 웹인 듯..

사용자가 직접 다루는 키오스크 앱은..? 테이블마다 태블릿을 갖다놓고 웹이나 앱으로도 만드는 것 같다. 매출관리 프로그램은 직원만 다루니 키오스크처럼 비주얼 UI를 신경 쓸 필요는 없겠지만.. 얘도 바뀌어 간다.
Delphi나 Visual Basic 같은 통상적인 RAD 툴에 대한 수요도 20년 전에 비해 확실히 줄어들었지 싶다.

단순 사전류 프로그램은 한컴사전밖에 안 남았고.. 도움말/문서는 빼박 다 웹이다. 로컬에다 제공하지 않는다.
Windows는 help 디렉터리에 두툼한 도움말 파일들이 사라졌고, Visual Studio의 몇 기가짜리 MSDN도 없어졌다. 2015쯤부터 말이다.

에구~~ 개인적으로는 오프라인 문서가 아예 없어져 버리면 심리적으로 좀 불편한데 말이다. 뭔가 붕 뜬 느낌이다.
종이책이 컴퓨터 viewer 기반으로 바뀌었을 때 약간 떴고, 컨텐츠가 이젠 내 하드에 저장조차 되지 않고 늘 인터넷 연결이 필요하다면.. 더 붕 뜬다. 이게 피할 수 없는 대세이긴 하지만..
이런 시국에 종이책이라든가, PC용 프로그램이 담당해야 할 영역이 무엇인가 하는 생각을 진지하게 하게 된다.

Posted by 사무엘

2023/11/17 08:35 2023/11/17 08:35
Response
No Trackback , 3 Comments
RSS :
http://moogi.new21.org/tc/rss/response/2231

오랜만에 또 C/C++ 문법 잡생각들을 늘어놓아 본다.

1. elaborated type specifier

C에서는 struct, enum, union 타입의 변수를 지정하려면 말 그대로 저 '종류' 명칭을 먼저 지정하고 나서 타입 명칭을 명시해야 했다. 종류 명칭을 생략하고 타입 명칭만으로 해당 종류를 나타내려면 C에서는 typedef를 번거롭게 해 줘야 했다.
그래서 C 시절에는 typedef struct _XXX { ... } XXX; 이런 두벌일이 관행이었다. struct _XXX라고 하든가, XXX라고 하든가 둘 중 하나다.

그러던 게 C++에서는 class라는 종류가 또 추가되었으며, 타입을 선언할 때 종류 명칭을 생략해도 되게 바뀌었다. struct XXX { ... }; 만 해도 XXX를 단독으로 쓸 수 있는 셈이다.
종류 명칭 지정은 required가 아니라 optional이 된 건데.. 허나, C++에서도 종류 명칭을 반드시 지정해야 할 때가 있다. 이런 full 명칭을 "elaborated type specifier"이라고 부르는데, 이게 필요한 상황은 바로 타입 명칭과 변수 명칭이 겹칠 때이다.

굉장히 의외이고 사실 권장되지 않는 관행이기도 하지만, C/C++에서는 기존 타입명과 동일한 명칭으로 변수를 선언하는 게 가능하다. (int, float 같은 built-in 타입 예약어는 당연히 제외)
ABC라는 클래스가 있다면 ABC ABC;라고.. ABC라는 이름의 객체/변수를 그대로 선언할 수 있다는 것이다. '야마토 급 전함 야마토'처럼 말이다.

두 클래스 A, B가 있고 앞에서 A B; 라고 B라는 변수를 선점해 버렸다고 치자.
이때 나중에 B라는 클래스의 인스턴스를 또 선언하고 싶다면 그때는 class B 뭐시기.. 이렇게 명시함으로써 이 B는 변수가 아닌 타입 명칭임을 알려줄 수 있다. A라는 클래스 소속의 변수 B, B라는 클래스 소속의 변수 A라고 상호 참조시키는 건 불가능하지 않으나 너무 사악해 보인다. -_-;;

전역변수와 지역변수가 이름이 겹칠 때 구분을 위해 :: 연산자를 사용한다면(C++ 한정), 변수명과 타입명이 겹칠 때 저런 종류 지정자가 쓰인다는 것이다.
내 개인적으로는 저 때야말로 typename 키워드도 사용 가능해야 하지 않나 생각하는데.. 그건 허용되지 않는 것 같다. ㄲㄲㄲㄲ typename과 class가 혼용 가능한(interchangable) 곳은 템플릿 인자뿐이다.

그 반면, 저기서는 struct와 class가 혼용 가능하다. 즉, class A라고 선언해 놓고는 elaborated type specifier로 struct A라고 쓰는 건 가벼운 경고 하나만 나오고 허용이다. 흥미롭지 않은지? =_=;; typename은 템플릿 바깥에서 범용적인 elaborated type specifier로서는 아직 접점이 없는 셈이다.

아울러, class는 자체적인 scope도 생성하는 역할을 한다. 그래서 :: 연산자에 잘못된 명칭이 지정됐을 때의 컴파일 에러는 "XXXX는 class 또는 namespace의 명칭이 아닙니다"이다. 요럴 때는 class가 말 그대로 namespace와 엮인다.
"class vs struct / typename / namespace"라니.. 이것도 흥미로운 점이다.

하긴, 변수명과 타입명이 겹치는 게 가능하니까 망정이지, 겹칠 수가 없다면 C 라이브러리의 struct tm (time.h)은 당장 이름이 바뀌어야 했을 것이다. 너무 짧고 겹치기 쉽고 성의 없게 만들어진 명칭이다. -_-;;

2. 정수형의 다양한 alias들

C/C++은 boolean 타입조차 없이 전부 int로 퉁치는 정수 덕후였다. 하지만 세월이 흐르면서 type-safety에 대한 필요성이 부각되었고, 용도에 따라 다음과 같은 alias 타입들이 등장해서 쓰이게 됐다.

(1) wchar_t (문자열): 유니코드 때문에 등장했고 얘 자체는 언어 표준으로 등극했다. wcslen, wcscpy 함수라든가, L"" 리터럴까지..
하지만 문자의 크기가 플랫폼별로 2바이트 내지 4바이트로 심하게 파편화됐다. 이 때문에 코드의 이식성을 저해하고 프로그래머들에게 큰 혼란을 끼치게 됐다.
결국 직접적인 크기를 명시하는 char16_t, char32_t가 나중에 일일이 추가됐다. 하지만 이것도 각 타입별 함수라든가 리터럴의 표기 방법, 심지어 % 문자열의 형식이 플랫폼마다 완전히 통일돼 있지 않다. 이식성 문제가 완전히 해결되지는 않았다는 뜻이다.

참고로 얘들은 다 built-in type이며, 기존 부호 없는 정수형의 단순 typedef가 아니다. 가령, char16_t의 포인터는 unsigned short의 포인터와 호환되지 않는다.
그리고 char이야 플랫폼 불문하고 무조건 1바이트라는 게 언어 스펙 차원에서 정의돼 있으니 char8_t를 또 만들 필요는 없다. 하지만 1바이트 문자열을 가리키는 char*는 처음부터 부호 없는 정수형으로 만들었으면 깔끔했을 텐데 하는 아쉬움이 좀 있다.

(2) ssize_t size_t (컴퓨터 비트 수): charXX_t처럼 일반 정수형도 크기를 명시한 intXX_t, uintXX_t 같은 게 도입됐는데, 얘들은 charXX_t와 달리 그냥 typedef이다.
그리고 64비트에서는 int와 long의 크기가 플랫폼별로 파편화돼 버린 관계로, 어디서나 포인터 크기와 동일함이 보장되는 정수형이 따로 만들어졌다. size_t라든가 intptr_t, uintptr_t, ptrdiff_t 말이다.
int를 4바이트로 유지시킨 건 그렇다 쳐도, long까지 32비트 4바이트로 굳힌 플랫폼은 Windows가 유일하다. 하위 호환성에 정말 목숨을 건 결정이다.

(3) time_t (미래 시간): 얘는 문자열이나 컴퓨터와 직접적인 관계는 없지만.. 그래도 21세기보다 훨씬 더 먼 미래를 표현하기 위해서 64비트로 확장되었다. time_t가 32비트이던 시절 기준으로 빌드된 구닥다리 프로그램들은 15년쯤 뒤 2038년 이후부터는 제대로 쓰기가 어려워질 것이다.
참고로 얘는 언제나 부호 "있는" 정수로 정의된다. 시각뿐만 아니라 두 시각의 차인 '시간'을 표현할 때도 쓰이기 때문이다. 과거와 미래를 모두 분간하려면 당연히 부호가 필요하다.

이런 숫자 alias들은 %문자와는 영 어울리지 않는다는 걸 알 수 있다. 저 typedef의 유동적인 비트수에 맞게 printf/scanf의 % 문자가 모든 플랫폼에 맞게 바뀌게 하려면... % 리터럴도 #define 해 가면서 바꾸면서 정말 지저분한 짓을 해야 된다. %ls인지 %S인지..?? %Id인지 %lld인지 %I64d인지.. 알 게 뭔가?

물론 값을 출력할 때는 모든 가변인자들이 intptr_t 크기로 promote되기 때문에 상황이 조금은 단순해진다. 하지만 입력을 받을 때라든가 32비트 플랫폼에서 64비트 값을 다룰 때는 역시 % 문자와 실제 변수 짝을 조심해서 대응시켜야 한다. 이러느니 C++ stream을 쓰고 말지.. =_=;;
그래도 %문자를 쓰는 게 다국어 지원 localize 관점에서는 취급이 아주 편리하다는 장점도 있는데 말이다. 차라리 독자적으로 % 문자 해석기를 만들기라도 해야 하나 싶다.

3. <=> 연산자

C/C++엔 ? : 이라고 유일하게 3개의 피연산자를 받는 독특한 연산자가 있다. if else문을 연산식 하나에다 박아 넣은 것이고, 오버로딩이 되지 않는다. 얘는 그냥 if else문만큼이나 C/C++의 문법처럼 취급되기 때문이다.
그런데, C++20에서는 단일 토큰으로서 길이가 3자나 되면서 연산 결과도 boolean 2종류가 아니라 '3종류'인 참 독특한 연산자가 추가되었다. 바로 <=> ... a <=> b는 a와 b의 대소 관계에 따라 1 0 -1 중 하나를 되돌린다. (실제로는 정확하게 정수형이 아니라 저 세 종류를 나타내는 comparision 객체 타입)
쉽게 말해 a, b가 문자열이라면 이 연산자의 결과는 strcmp 함수의 결과와 같다.

연산식에서 이 연산자가 당장 막 쓰이지는 않을 수 있다. 그러나 어떤 클래스를 구현할 때 이 연산자는 굉장히 유용하게 쓰일 것 같다. 얘는 온갖 자잘한 비교 연산자들의 상위 호환이기 때문이다.
<=> 연산자 하나만 오버로딩 해 놓으면 > < >= <= == != 을 모두 유추할 수 있다. a==b는 a<=>b == 0 이렇게 말이다.

이 연산자가 지원되는 클래스는 Java로 치면 Comparable 인터페이스를 받아서 CompareTo 메소드를 구현한 거나 마찬가지일 것이다.
C의 사고방식이라면 이 함수의 리턴값은 그냥 int이겠지만.. 얘는 C++의 이념이 가미됐다 보니 built-in 연산자의 리턴 타입이 언어 차원에서 따로 정의돼 있다.

Visual C++에서도 최신 C++20 표준 문법 옵션을 켜 주면 바로 써 볼 수 있다.
외국에서는 <=> 가 무슨 우주선(!!!!)처럼 생겼다면서 spaceship operator이라는 애칭으로 불리는가 보다.
10여 년 전엔 R-value 참조자 &&가 아주 참신하게 느껴졌는데 지금은 쟤가 비슷하게 참신하게 느껴진다.

4. 나머지 C

(1) 비트필드에 배열이 지원됐으면 좋겠다는 생각을 하는데.. 5비트씩 n개 같은 식으로 말이다. 이건 너무 욕심 부린 걸까..?? ㅎㅎ
뭐, 컴파일러의 입장에서 코드를 생성하는 게 힘들 수는 있지만.. 그래도 불가능하지는 않을 텐데 말이다.
아키텍처에 따라서 멤버들 방향 지정을 자동화하는 것과 더불어 개인적으로 비트필드에 바라는 사항이다.

(2) 배열의 원소 개수를 구하는 arraysize, 그리고 배열에서 특정 멤버의 오프셋을 구하는 offsetof
이거는 언어의 기본 문법과 연산자만으로 구현 가능하기 때문에 딱히 예약어로 지정돼 있지는 않다.
하지만 최소한 표준 라이브러리에 채택돼서 표준 헤더에서 제공할 만은 해 보인다. 특히 arraysize의 경우, C에서는 그냥 x/x[0] 같은 매크로로 구현되겠지만 C++에서는 더 type-safe한 인라인 템플릿 함수로 제공되면 될 것이다.

(3) C에는 자기 번역 단위의 밖으로 노출되지 않는 static 변수와 함수가 C++ 사고방식으로 치면 private 멤버와 얼추 비슷한 지위이다.
static 함수가 한 소스 파일 안에서 선언되고 참조(= 호출)도 됐는데 그 함수의 몸체가 정의돼 있지 않으면?? 이건 링크 에러가 아니라 해당 번역 단위에 대한 컴파일 에러로 처리된다. 오오~!! 다른 번역 단위들을 뒤질 필요가 없기 때문이다.
C++로 치면 unnamed 익명 클래스라든가 함수 안의 local 클래스에서 멤버 함수의 몸체가 곧장 정의되지 않은 것과 비슷한 상황이다. 이런 일회용 클래스들은 함수의 몸체를 바깥 딴 데서 찾을 만한 여지가 없다. ^^

C와 C++에서 이런 캡슐화 패러다임의 차이가 드러날 때가 있다.
한 클래스 A의 내부에서만 쓰이고 마는 내부 클래스 B를 그냥 A.cpp 안에다가 global scope로 선언할지, 아니면 A가 선언된 A.h 헤더 파일에다가 A 내부의 scope로 private 선언할지 말이다.
객체지향 이념에 따르자면 헤더 파일에다가 선언하는 게 좋지만, 실용적으로는 그냥 cpp가 낫다. 헤더에다가 넣으면 외부에 노출되지 않는 클래스인데도 수정할 때마다 그 헤더 의존하는 소스 파일들이 다 빌드되니까 말이다.

5. 나머지 C++

(1) "한 번도 참조되지 않은 변수"라고 경고(컴파일러 또는 정적 분석에 의해)가 뜨는 걸 무시하기 위해서 [](...){}(a,b,c,d,e); 라는 람다가 쓰인다니 참 대단하다. 아울러,
auto convert(const istream &input)  -> void;
void convert(const istream &input);

클래스의 멤버 함수도 이렇게 람다 스타일로 선언할 수 있으며, 위의 둘은 완전히 동치라고 한다. typedef 대신 using을 쓰는 문법과 비슷해 보인다. ㄲㄲㄲㄲㄲ

(2) 그나저나 using은 typedef의 완벽한 상위 호환이어서 typedef는 이제 쓸 필요가 전혀 없어지는 건지? signed 같은 잉여가 되는 건가 싶다. 템플릿 인자에서 class가 typename으로 대체되고 static 함수가 익명 namespace 함수로 바뀌는 것과 비슷한 양상인데, typedef는 쟤 말고는 다른 용도가 전혀 없으니 말이다.
using A = B는 파스칼에서 type A = B와 형태가 아주 비슷해 보이기도 한다.

(3) C++의 iterator들은 어지간한 건 내부 구현이 그냥 포인터 하나와 다를 바 없을 텐데.. intptr_t 같은 정수 하나로 간단하게 reinterpret_cast가 가능했으면 좋겠다. 그래야 type-safe하지 않은 C 스타일 콜백 같은 데서도 내부적으로 C++ 컨테이너의 원소에 접근할 수 있기 때문이다.
특히 list, vector 말이다. hash는 모르겠다만.. 트리 기반 컨테이너인 set, map은 그 특성상 노드들이 parent 노드 포인터까지 갖고 있는데, iterator도 포인터 하나만 갖고 있어도 다음 진행 방향을 결정할 수 있지 않은가?
하지만 포인터 하나보다 크기가 더 큰 iterator도 심심찮게 보이는 것 같다.

(4) constexpr은 C++도 단순 read-only와 진정한 constant의 구분을 두려는 시도인 듯하다. 게다가 멀쩡한 함수를 '인라인화'도 모자라서 컴파일 시점에서의 상수로 바꾼다니..
팩토리얼이나 피보나치 수열 상수를 재귀적으로 구하는 건 예전에는 템플릿 클래스의 상수값 형태로나 가능했다. 하지만 이제는 C/C++ 상으로 멀쩡하게 생긴 함수의 호출 형태로도 표현 가능해졌다.
뭐, 템플릿에서도 static_assert와 더불어 많이 활약할 것으로 예상되는데, 자세한 건 더 공부해 봐야겠다.

(5) 객체를 초기화할 때 생성자 obj(arg)나 대입 연산 obj=arg 말고 중괄호는 배열이나 구조체를 초기화할 때에나 쓰이는 물건으로 여겨졌다. 하지만 C++11부터는 이게 initializer list라는 개념으로 리모델링되어 임의의 클래스의 public 멤버들을 순서대로 초기화할 때도 쓰고, 컨테이너에다 여러 원소들을 한꺼번에 집어넣을 때도 쓰일 수 있게 됐다.
참 혁신적이긴 하지만 용도가 너무 다양한 것 같다. 모호성이 발생하지는 않는지, {...}는 그럼 R-value 리터럴인 건지, 내가 만드는 클래스에서 저런 걸 받아들이려면 어떡해야 하는지 궁금한 게 많다. 이것도 공부 필요.. =_=;

(6) 인터페이스를 여러 개 받아서 구현한 클래스가 정작 그 인터페이스들의 base로는(예: IUnknown) 모호하다고 형변환 되지 않는 오류 말이다(Visual C++ 기준 C2594). 정말 아무 의미 없고 멍청한 페이크에 가까운 오류인데..
base가 고유한 vtbl이 없고 데이터 멤버도 없다면 그냥 자기 this에서 가장 가까운 base를 언어 차원에서 알아서 지정하게 하는 게 좋지 않을까? 애초에 자기 데이터가 없는데 가상 상속을 할 필요도 전혀 없는걸? 궁금하다.
이게 언어 차원에서 interface라는 게 없고 그 대신 무식한 다중/가상 상속을 지향하며 만들어진 C++의 맹점인 것 같다.

(7) 나는 C/C++ 문법을 어지간한 건 다 마스터 해서 머리에 숙지하고 있고, 아무 코드나 보면 머릿속으로 가상의 컴파일러를 돌려서 "얘는 이런 식으로 기계어로 번역되겠다, 구현 비용이 얼마나 되겠다, 이렇게 동작하겠다, 이런 문제가 있다" 같은 게 예측이 된다고 생각해 왔다. 넓은 의미에서 암산과 비슷한 경지일 것이다. 아 당연히 난해한 코드 출품작 급의 괴물 코드 말고, 평범한 코드 말이다. -_-;;
하지만 계속해서 새로운 기능, 기괴한 기능들이 추가되고 있는 modern C++을 보면 이런 자신감이 갈수록 줄어드는 것 같다. 배배 꼬인 템플릿에다 auto에 람다에, ...에 헥헥~ 이 기능은 어떤 문법적 근거를 통해 빌드 되는 건지부터가 파악이 안 되는 것도 있다. =_=;;

요즘 C++은 정말 옛날에 내가 알던 그 C++에서 갈수록 멀어져 간다. 그 경직된 정적 타입 네이티브 코드 컴파일 언어에서 어떻게 동적 타입 언어의 유연함을 집어넣은 걸까? 특히 가변 인자 템플릿 말이다.;; (튜플!!) ㄷㄷㄷ

Posted by 사무엘

2023/11/14 08:35 2023/11/14 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2230

1. 자연어 처리

우리나라 헌법 전문은 무진장 길고 복잡한 만연체인데, 결국 핵심은 "대한국민은 헌법을 개정한다"이다. ㄲㄲㄲㄲㄲㄲ
자, 문장 구조를 파악하기 위해 프로그램 코드처럼 들여쓰기를 적용해 주면 얼추 다음과 같다.

. 유구한 역사와 전통에 빛나는
우리 대한국민은
. . . . . 3·1운동으로 건립된
. . . . 대한민국임시정부의 법통과
. . . . . 불의에 항거한
. . . . 4·19민주이념을 계승하고,
. . . . . 조국의 민주개혁과 평화적 통일의 사명에 입각하여
. . . . . 정의·인도와 동포애로써
. . . . 민족의 단결을 공고히 하고,
. . . . 모든 사회적 폐습과 불의를 타파하며,
. . . . . 자율과 조화를 바탕으로
. . . . 자유민주적 기본질서를 더욱 확고히 하여
. . . . . 정치·경제·사회·문화의 모든 영역에 있어서
. . . . 각인의 기회를 균등히 하고,
. . . . 능력을 최고도로 발휘하게 하며,
. . . . . **자유와 권리에 따르는
. . . . 책임과 의무를 완수하게 하여**,
. . . . **안으로는
. . . 국민생활의 균등한 향상을 기하고
. . . . 밖으로는 항구적인
. . . 세계평화와 인류공영에 이바지함으로써**

. . . 우리들과 우리들의 자손의
. . 안전과 자유와 행복을 영원히 확보할 것을 다짐하면서
. 1948년 7월 12일에 제정되고 8차에 걸쳐 개정된
헌법을
. 이제 국회의 의결을 거쳐
. 국민투표에 의하여
개정한다.

안팎드립을 비롯해 몇몇 좋은 훈계조 문구들은 국민 교육 헌장에서 모티브를 딴 것 같다~!

"... 안으로 자주독립의 자세를 확립하고, 밖으로 인류 공영에 이바지할 때다."
"... 자유와 권리에 따르는 책임과 의무를 다하며 ..."


다만, 이런 요지의 문장 자체는 제헌헌법 시절부터 있었다고 한다. 따지고 보면 국민 교육 헌장이 기존 헌법 전문으로부터 영향을 받은 것이다.

2. AI

AI가 앞으로 의료나 법조계 직업을 많이 빼앗을 거라고 흔히들 예측한다. 특히 사법 불신이 팽배하다 보니 법 쪽은 "대체될 것이다"가 아니라 "당장 대체돼야 된다"라고 강하게 주장하는 사람도 있다.
하지만 세상 돌아가는 양상을 보면 그런 분들의 바람은 가까운 미래에 호락호락 이뤄질 것 같지 않다.

AI 기술의 침투가 제일 소극적이고 더디고 분야, 제일 신중하고 보수적으로 행해지는 분야가 바로.. 사람의 돈이나 인생, 목숨이 왔다갔다 하고 법적 책임이 부과되는 크리티컬한 분야이기 때문이다. 의· 법이 권위 있는 직종인 이유도 바로 이 때문이다.
10살짜리 초딩이 무슨 미적분 할아버지 문제를 풀고 자동차 내부 구조를 달달 외운다 해도, 걔한테 대형 트럭 운전 면허증을 주지는 않는 것과 비슷한 이치이다. 고졸 일자무식이어도 법적 책임 능력이 있는 성인이 그런 차를 몰지.

기계 및 기계 관리자를 전적으로 믿을 수 없어서 자율주행이나 전자투표도 호락호락 정착하지 못하고 있는데, 진료나 판결을 AI가 덜컥 한다..???
솔직히 AI가 수많은 판례들을 학습해서 일관성 있는 합리적인 판결을 내릴 기술적 역량은 이미 갖춰져 있다. 허나 AI라고 해서 흉악범을 몽땅 속 시원하게 사형 때리지도 않을 뿐더러, 그와 별개로 그 분야에서의 AI 대체는 정서상 절대로 금방 되지 않을 것이다. 그런 분야에서 AI의 개입은 기껏해야 "참고만 할 것. 아니면 말고" 수준인 법률 자문, 조언, 보조 수준에서 그칠 것이다.

저런 분야 말고 상상화를 너무 기괴하게 그렸다고, 바둑 게임에서 상대편에게 졌다고, 애한테 조금 허언증스러운 잘못된 정보를 가르쳤다고 해서 당장 사람이 죽고 탈 나지는 않는다. 그런 분야는 AI가 이미 넘치도록 활개를 치고 있다.

3. 정보 보안

입구에 '신천지 OUT 출입금지' 딱지가 붙어 있는 교회를 보면.. 참 웃픈 생각이 든다.
작정하고 기존 교회 신자들을 빼 가려고 침투하는 신천지 공작원(?)이 그걸 보곤 참 잘도 겁 먹고 꽁무니를 빼겠다.

울나라 군사분계선 근처에다가 '북한군 접근 금지'라고 딱지 붙여 보지 그래..?
그런 곳에다가는 보통은 북한군이 아니라 '허가받지 않은 민간인은 접근 금지'라고 써 붙이는 게 자연스럽다. 둘의 차이가 뭔지 모르는 분은 없으리라 믿는다. 암튼..

내가 출처를 직접 확인해 보지는 못했지만.. 마틴 루터가 "이단은 무력이 아니라 설교로 퇴치해야 된다"라는 말을 남겼다고 한다. "펜은 칼보다 강하다"와 같지는 않지만 비슷한 관점에서 참으로 옳은 말이다.

이단들이 물리적으로 꽹과리 치고 흉기 휘두르면서 타 교회 예배를 방해하고 집기를 파손하고 신자들을 해친다면야.. 그럼 당연히 경찰에 신고해야 되고 공권력에 호소하며 도움을 요청할 수 있다. 세상 법정에다 손해배상 소송 걸어서 깽값 받아낼 수도 있다. (받아내야 한다.. 가 아니라 그럴 수도 있다고)

그러나 그러나~~ 계시록 14만 4천 명이 누군지, 동방의 의인이 누군지.. 새 하늘과 새 땅이 뭔지, 열두 지파 정체가 뭔지.. 그딴 거 해석하고 판단하는 일을 세상 경찰이나 법원에다 맡길 참인가? 그런 걸 자기와 다르게 생각하는 이교도 이단들을 과태료 물리거나 민형사 책임을 물어서 처벌이라도 할 생각인가?

교리를 똑바로 가르쳐서 자기 성도들이 신천지 추수꾼들한테 홀딱 속지 않게 해야 하지, 그러지도 않으면서 무책임하게 '신천지 출입금지' 이러는 건 교회가 자기 할 바를 다하지 않고 세상 공권력에다가 이단 퇴치를 다 떠넘기는 것처럼 보인다. 무슨 웹사이트들 하단에 관행적으로 쓰여 있는 "이메일 주소 무단수집 거부"도 아니고 참..;;

신천지라든가 공산주의나 동성애 따위가 아무리 나쁘다고 해서 그걸 무슨 세상 공권력이나 타 종교하고까지 손잡아서 물리적으로 박멸하는 것은 기독교회가 해야 할 '하나님의 일'이 아니다. 당장은 그게 편해 보여도 세상 공권력은 언제든지 기독교를 박해하는 역공 비수가 되어 교회로 되돌아올 수 있다~!

그건 그렇고.. 컴퓨터 정보 보호 보안의 관점에서도 이거랑 아주 비슷하게 느껴지는 원칙이 있다.
암호 보안은 '암호화 알고리즘'이라는 코드가 아니라, '거기에다 준 암호화 키'만으로 완벽하게 보장돼야 한다는 거.

"이 데이터는 우리 회사만의 기가 막힌 블랙박스 알고리즘으로 암호화돼 있습니다. 보안을 위해 구체적인 알고리즘은 공개할 수 없습니다" 이건 저런 "신천지 출입금지" 딱지를 거는 것과 비슷한 수준 낮은 보안이다. -_-;;; 지난 2009년, 코드소프트 암호 크랙 공모전 흑역사처럼 말이다.

다시 말하지만 암호화 알고리즘은 무슨 요리 비법마냥 장인의 손맛이 담긴 비기 같은 존재가 절대 아니다~!! ㄲㄲㄲㄲㄲ
실제 정보 보호 업계가 돌아가는 방식은 이와 전혀 다르다. 모든 암호화 알고리즘들이 소스째로 다 투명하게 버젓이 풀려 있다. 아무라도 그 코드를 돌려서 임의의 데이터를 임의의 키로 암호화 복호화한 결과를 볼 수 있다.

이 데이터는 무슨 알고리즘으로 암호화됐는지 알려져 있고, 그 알고리즘 코드도 이미 다 있다.
그럼에도 불구하고 무슨 키로 암호화됐는지를 모르면.. 일일이 모든 키로 다 해독해 보는 brute force 외의 방법으로는 원래 데이터를 절대로 복원할 수 없다.

이게 "이단은 설교만으로 퇴치해야 된다"와 같은 급의 보안인 것이다. 암호화 키가 무슨 교리 같은 존재인 듯.. ^^
히틀러인지 누군지 아무튼 어느 나치 독일 간부가 남긴 명언(?) 중에 "힘은 방어가 아니라 공격에 있다"가 있었던 것 같은데.. 그 말을 응용해 보면 "보안은 알고리즘이 아니라 키에 있다" 정도가 될 것이다.

여담:
여기까지 글을 썼더니 현업에서 목회를 하시는 분들께서 글 주제와 관련하여 여러 조언 첨언을 본인에게 해 주셨다.

  • 그냥 자기가 소속된 교단에서 딱지 붙이라고 반강제 강권해서 붙이는 편이라고 한다. 오키. 거기까지는 이해가 되는데..
  • 심지어 신천지 교회들이 '위장'을 위해서 자기 예배당 입구에다가 "신천지 OUT" 딱지를 붙이는 경우도 있다고 한다. 미친~~ ㄷㄷㄷㄷㄷㄷ
  • 신천지에서는 기존 교회들에다가 스팸성 우편물도 끈질기게 왕창 보낸다고 한다. 흐미~ 도대체 뭔 재정으로 저럴 여력이 있는지 모르겠다.

Posted by 사무엘

2023/10/30 19:35 2023/10/30 19:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2225

컴퓨터의 정보 기억장치는 다음과 같은 속성에 따라 분류 가능하다.

(1) 배열처럼 아무 지점이나 O(1) 시간 복잡도로 즉시 접근 가능한가? 아니면 링크드 리스트처럼 순차적으로만 접근 가능한가?
Random Access memory라는 건.. 아무렇게나 읽고 쓰기가 가능하다는 뜻이 아니라, 임의 지점 접근이 가능하다는 뜻이다.
물론 오늘날은 테이프 같은 극단적인 물건 말고는 RAM이 당연시되고 있다. 테이프는 임의 접근이 안 되니 번거로운 감기 기능이 필요했지만.. CD는 아무 트랙이나 바로 갈 수 있다.

(2) 읽고 쓰기 가능한가, 아니면 읽기 전용인가?
ROM이라고 불리는 물건들은 RAM의 속성을 지니면서 동시에 ROM인 셈이다. ROM은 RAM의 엄밀한 반의어가 아니니 유의할 것.
CD 같은 광학 디스크는 요즘 기술로 '쓰기' 자체는 가능하다. 하지만 아무 부작용이나 부담 없이 자유자재로 아무렇게나 쓸 수 있는 건 아니다.

(3) 휘발성인가, 비휘발성인가
아주 중요한 속성이다. 전원이 끊어지면 내용이 싹 다 날아가느냐, 아니면 그 뒤에도 내용이 남아 있느냐? 읽기 전용 메모리는 당연히 비휘발성이어야 할 테니 이건 읽쓰 겸용 메모리가 대상이다.
반도체 기반의 주메모리는 속도가 빠른 대신 전자이고, 나머지 보조 기억장치들은 느린 대신 용량 많고 후자의 속성을 지닌다.

(4) 매체와 reader/writer가 쉽게 분리 가능한가? 아니면 붙박이인가?
이거 무슨 철도 차량으로 치면 기관차-객차 vs 동차 같은 차이점 같다.

(5) 그리고.. 어떤 기술 배경에 따라 만들어졌는가?
다음과 같이 세 계열로 나뉜다.

1. 자기장: 테이프, 디스켓 // 디스크, 드럼.

매체 분리형에서는 테이프만이 무슨 방송국이나 데이터센터 급의 백업/아카이빙 용도로 쓰이고 있고 나머지는 도태했다. 디스켓이고 zip드라이브고 뭐고 다 망했으니까. 붙박이형은 디스크만이 '하드'의 형태로 남고 다 도태했다.
1950년대에 슈퍼컴퓨터 용으로 무려 5MB짜리 하드디스크를 지게차에다 조심스럽게 실어 나르던 시절을 보면 참 격세지감의 극치가 따로 없다.

사용자 삽입 이미지

여기는 전통적인 트랙이니 섹터니 하는 구조 구분과 '포맷'이라는 게 통용되는 기억장치이다. 하드디스크는 실린더라는 것도 있었고 말이다.
똑같은 디스켓이라도 운영체제에서 소프트웨어적으로 공간 구획을 구분하고 인식하는 방법이 다르다. 과거에는 플랫폼과 운영체제에 따라 이런 파편화가 더 심했기 때문에 디스켓들이 포맷되지 않은 채로 판매되곤 했다. 물론 IBM PC와 MS-DOS가 천하를 평정한 뒤부터는 디스켓들이 다 미리 포맷되어서 나오기 시작했다.

하드디스크는 전원이 끊어질 때 안전을 위해 파킹이라는-_- 마무리 동작도 권장되곤 했다. 물론 훗날 자동 파킹이 지원되면서 별도의 파킹 유틸리티는 화면 보호기만큼이나 별 필요 없는 눈요기 잉여로 전락했다.

2. 반도체: USB 스틱, SD카드 // SSD

메모리 반도체는 100% 전자식으로만 동작하는 물건이다. 빠른 대신에 비싸고, 무엇보다도 그 특성상 전기가 끊어지면 내용도 다 날아가는 '휘발성 메모리' 전용이었는데.. 기술의 발달로 보조 기억장치 역할도 가능한 메모리 반도체가 등장했다.
얘 덕분에 기존의 테이프나 디스켓이 완전히 전멸해 버렸다. 그리고 SSD도 가격 내려가고 용량 올라가면서 기존 기계식 하드디스크의 입지를 상당수 위협하고 있다.

SSD는 조각 모음이 필요하지 않으며, 동작하는 특성이 기존 디스크와는 많이 다르다.
USB 스틱은 매체와 구동부가 일체형인 반면, SD카드는 매체와 구동부가 분리돼 있다. (별도의 reader가 필요)
옛날 8비트 시절에 게임용으로 쓰였던 롬팩 카트리지도 1번이 아니라 2번 반도체 기반이었던 거지..??

전자기기에서 캐퍼시터(축전기)와 본격적인 화학 전지의 관계가 반도체 메모리와 타 보조 기억장치의 관계하고 비슷해 보인다~!!
전자는 충전· 방전이 아주 빠르고 용량이 아주 작으니까. 그리고 캐퍼시터를 용량을 왕창 키워서 배터리처럼 사용하려는 연구가 진행 중이기도 하다.

3. 광학(레이저)

얄팍하고 비까번쩍 빛나고 뭔가 하이테크스럽게 생긴 원반이다. 1990년대에 첫 등장했을 때는 얼마나 간지 뽀대 났겠는가.

사용자 삽입 이미지

얘는 그 특성상 붙박이라는 게 존재하지 않고 드라이버와 매체가 분리돼 있다. 기억장치들 중 디지털 방식의 음반· 영상매체와 가장 친화적이라는 특징도 있다. (그 반면, 테이프는 '아날로그' 방식의 매체..)
얘는 '쓰기'와는 그렇게 친화적이지 않다. 디스크에다가 작정하고 새로 기록 추가만 가능하며, 한번 새겨 버린 내용을 자유자재로 덮어쓸 수 없다. 평범한 디스크 저장이 아니라 종이에다 인쇄하는 것과 얼추 비슷하다고 생각해야 한다.

디스크의 영어 스펠링은 disk와 disc가 혼용되는 듯한데.. disc는 특별히 대놓고 원반 모양인 광학 매체에 한정되어 쓰이는 것 같다. 가령, 하드는 hard disk이지만, CD는 compact disc이다.
그리고 CD(+ DVD, 블루레이)는 지름이 12cm인 반면, 과거에 있었던 레이저 디스크는 지름이 12인치였다는 아주 흥미로운 차이점이 있다. 뭐, 거기에다 미니CD라고 지름 8cm짜리 규격도 있긴 하고 말이다.

한때 광학 기억장치는 용량이 방대하다는 장점이 있었지만 오늘날은 빛 바랜 장점이다. 이제는 컴퓨터에서 광학 드라이브 자체가 거의 퇴출되었고, 운영체제 설치도 그냥 네트워크나 USB로 다 되는 세상이 됐다.
그래서 DVD의 다음 규격인 블루레이는 용량이 더 방대함에도 불구하고 오늘날 존재감이 매우 미미하다. 블루레이에다가 수십 GB짜리 패키지 게임을 담아서 판매하는 시대도 아니니 말이다.

옛날에는 뭔가 레이저를 사용하는 컴퓨터 주변기기는 가격이 억소리 나게 비쌌던 걸로 악명 높았다. 레이저 프린터, 그리고 씨디 라이터.. 그게 어쩌다가 가격이 확 떨어지고 개인용 컴퓨터에 씨디를 굽는 기능까지 내장되어 들어갔는지.. 경이롭기 그지없다.
기껏 들어갔던 기능이 이제는 필요 없어져서 퇴출되는 지경이고..

※ 여담 1: 옛날 추억 더

  • 1990년대에 VGA 이후로 SVGA 그래픽 카드들이 표준 규격 없이 난립했던 것처럼.. 확장 디스켓도 표준 규격 없이 너무 난립했던 것 같다. 더구나 USB니 plug & play니 없던 시절에는 하드나 디스크 드라이브를 하나 더 장착하는 것도 엄청나게 어렵고 컴퓨터 하드웨어 지식이 많이 필요하던 과업이었다. 그러니 그런 싸제 물건들이 성공적으로 보급되기가 어려웠다.

  • 그나저나 이 바닥은 자동차 브레이크 말고도 '디스크와 드럼'을 쌍으로 구경할 수 있는 또 다른 분야인 게 신기하다. '자기 드럼'은 어떤 형태로 동작하는 물건이었을까..?? 개인적으로 무척 궁금하다.

  • 옛날에는 테이프나 디스크에 물리적인 쓰기 방지 탭이나 딱지 같은 게 붙어 있기도 했지만.. 요즘은 그런 관행을 찾을 수 없다. (운영체제 셸이 그런 데다가도 메타데이터나 썸네일 캐시 같은 걸 임의로 써 넣곤 함)

  • 옛날에는 광자기 디스크라는 것도 있었던 것 같은데.. 뭐지? 1번과 3번의 하이브리드인가 싶다.

카세트 테이프(자기장)나 롬 카트리지(반도체)는 8비트의 산물이다. 16비트 IBM 호환 PC급에서 저런 것들을 취급하는 사례는 내가 아는 한 없다.
그 대신 디스켓 FDD는 컴퓨터 붙박이 형태로 16비트 이후 시대를 풍미했다가 64비트 시대에는 사실상 전멸했다.
CD-ROM은 16비트 도스 시절에도 존재하긴 했지만 mscdex니 뭐니 하는 굉장히 무거운 드라이브를 실행해야 사용 가능했다. 그리고 386, 486급 이상 PC의 전유물이었다.
그 뒤로 USB 메모리는 도스와의 유의미한 접점이 없다. ^^

아무리 생각해도 1990년대 중후반에 plug & play와 USB는.. 2000년대 중후반의 64비트와 멀티코어하고 굉장히 비슷한 관계인 것 같다.
서로 담당하는 분야는 다르지만 결국 비슷하고 관련 있는 성격의 기술이었다는 점에서 말이다.

※ 여담 2: 정보 저장이라는 관점에서 성경 본문 고찰

성경에는 뭔가 정보 기록 매체를 암시하는 얘기가 있다. 가령, 요한복음의 21장 25절 제일 마지막 구절은 이렇다.
"예수님께서 행하신 다른 일들도 많으므로 만일 그것들을 낱낱이 기록한다면 심지어 이 세상이라도 기록된 책들을 담지 못할 줄로 나는 생각하노라."

"그 크신 하나님의 사랑"이라는 찬송가의 3절 가사는 이렇다.
"하늘을 두루마리 삼고 바다를 먹물 삼아도 한없는 하나님의 사랑 다 기록할 수 없겠네"

흠, 책이 아니라 SD카드라면 어떨까? 자기 테이프라면 어떨까..??
혹시 생명책이 실제로는 책이 아니라 무슨 SQL 서버가 돌아가는 IBM 메인프레임인 건 아닐까?

Posted by 사무엘

2023/10/05 08:35 2023/10/05 08:35
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2215

컴퓨터 알고리즘 문제 중에는.. "N개의 원소로 구성된 목록에서 majority.. 과반/다수파라는 게 존재하는가? 존재한다면 무엇인가?"를 구하는 문제가 있다.
목록에서 과반의 원소가 다 같은 값이면 그게 바로 majority라고 정의된다. 원소들은 꼭 대소 관계가 성립할 필요가 없고 그냥 동등성 판단만 가능하면 된다. 그러니 꼭 정수가 아니어도 된다.

또한, 반반이 아니라 과반이라는 특성상, majority는 존재하지 않거나, 유일하게 존재.. 언제나 둘 중 하나이다. 공동 1위 같은 것도 고려할 필요가 없다.

이 문제는 뭐랄까.. 단순하면서도 참 므흣하다.
일단, "목록에서 가장 자주 등장하는 원소--등장 빈도수가 가장 큰 원소를 구하시오"라고 접근하지 않는 게 핵심이다.
각 원소들의 빈도수를 일일이 관리하자면 알고리즘의 시간 복잡도가 기본이 O(n^2)에서 시작할 것이고, 균형 잡는 tree 기반의 컨테이너를 사용한다 하더라도 O(n log n)이 한계이다.

그러나 이 문제를 풀기 위해서는 일을 그렇게 크게 벌일 필요가 없다.
각 원소의 빈도수가 아니라.. "이 목록은 과반의 원소가 그냥 동일한 값인가?"라고 접근하는 게 좋다.

수학인지 논리학인지 거기에는 "비둘기집의 원리"라는 게 있다. N+1개의 물건을 N개의 상자에다 다 집어넣는다면, 적어도 한 상자에는 그 물건이 2개 이상 들어가 있다. 뭔가 미적분에서 말하는 중간값 정리처럼.. 너무 당연한 말 같은데 말이다.
그것처럼 어떤 목록에 같은 원소가 "과반"이라면 그 목록은 다음 둘 중 한 특성을 반드시 갖게 된다.

  • 과반의 원소가 아무리 고르게 분산되어 분포한다 하더라도, 그 원소가 연달아 두 번 이상 등장하는 구간이 반드시 하나 이상 존재한다~! 1 1 2 1 특히 원소의 전체 개수가 짝수라면 이건 뭐.. 무조건 빼박이다.
  • 만약 그게 아니라면.. 그냥 맨 마지막 원소가 다수파이다.

어, 정말 저렇게 단정할 수 있나 의아할 텐데.. 이런 과감한 주장은 다수파의 정의가 절반의 '초과', '과반'이기 때문에 성립 가능하다.
절반을 포함하는 '이상'이기만 해도 위의 조건들은 당연히 성립하지 못하게 된다. "1 2 1 2" 같은 것만 생각해 봐도 알 수 있다.

이렇듯, 다수파가 존재할 때 가질 수밖에 없는 목록 전체의 특성을 생각하면.. 다수파를 굉장히 단순한 절차만으로도 정확하게 구할 수 있다.

  • 최초엔 맨 첫째 원소가 다수파라고 가정하고 후보로 지정한다. 연속 등장 횟수(이하 점수)도 1을 부여한다.
  • 그 다음 원소가 후보 원소와 동일하면 점수를 1 증가시킨다. 그렇지 않으면 점수를 1 감소시킨다.
  • 단, 이미 현재 점수가 0이 된 상태여서 더 감소시킬 것이 없으면 후보 자체를 지금의 새 원소로 교체한다. 그리고 점수를 1로 다시 부여한다.
  • 이 과정을 모든 원소들에 대해 수행한 뒤, 현재 지정되어 있는 후보를 결과값으로 되돌린다.

사용자 삽입 이미지

위키백과에는 이 과정을 다음과 같이 시각적으로 잘 묘사한 그림이 있더라.
정말 허무할 정도로 단순하다. 이 알고리즘은 고안자의 이름을 따서 Boyer-Moore majority vote algorithm이라고 명명되어 있다. 1981년에 학계에 처음으로 발표됐다고 하는데.. 동작하는 방식을 보니 후보, vote 이란 워딩이 적절해 보인다.

Boyer-Moore 이거 혹시 "문자열 검색 알고리즘에도 나오는 이름이 아닌가?"라는 생각이 들 텐데.. 정확하다. 동일한 명칭이다. ㄲㄲㄲㄲ

단, 위의 알고리즘은 목록에 다수파라는 게 실제로 존재하지 않더라도 언제나 후보를 갖고 있다가 되돌린다. 그러니 목록에 다수파가 존재한다면 정확한 답을 되돌리지만, 애초에 다수파가 존재하지 않는다면 뭔가 임의의 엉뚱한 후보를 되돌린다.

그렇기 때문에 이 알고리즘에는 2nd-pass, 즉 후처리라는 게 필요하다. 앞의 1st-pass를 통해 구해진 후보가 진짜로 다수파가 맞는지, 얘만 개수를 처음부터 다시 세어 보는 것이다.
1st-pass 때 쓰였던 점수 변수는 증가했다가 감소하기를 반복했기 때문에 정확한 개수가 담겨 있지 않다.
이거 무슨 분수· 무리방정식을 풀고 나서 검산을 해서 무연근을 제거하는 것과 비슷한 느낌이다.

자, 두 pass 모두 시간 복잡도는 O(n)이고, 공간 복잡도는 지역변수 꼴랑 한두 개.. O(1)에 지나지 않는다. 정말 놀랍지 않은가?
얘는 알고리즘 자체는 쉽게 이해할 수 있지만, 정말 이렇게만 해도 언제나 문제에 대한 정확한 답이 구해지는지 correctness를 이해하는 게 좀 빡셀 수 있는 문제이다.

알고리즘이라 하면 복잡한 기법을 동원해서 무식하게 풀었을 때 O(n^2)짜리인 것을 O(n log n)으로 낮춘다거나, 팩토리얼/지수함수 급인 것을 O(n^2)나 O(n^3)으로 낮추는 형태인 게 많다.
그러나 최적화를 통해서 이렇게 O(n)을 만들 수 있는 문제는 흔치 않아 보인다.

이 다수파 구하기와 성격이 아주 비슷한 문제는 N개의 숫자 목록 내부에서 "합이 가장 큰 연속된 구간을 찾기"인 것 같다. 당연히 양수와 음수가 뒤죽박죽 섞여 있는 목록에서 말이다.

정답이 (x~y) 구간은 그야말로 1<=x<=y<=N 아무렇게나 가능하기 때문에 이것도 언뜻 보기에는 시간 복잡도가 O(n^2)이나 최하 O(n log n)이 될 것 같다. 그러나 얘도 아주 간단한 검사만 하면서 시간 복잡도 O(n)과 공간 복잡도 O(1) 만으로 아주 빠르게 풀 수 있다.

  • 정답 구간이 맨 첫 원소에서 시작된다고 가정하고 각 원소들을 쭉쭉 더해 본다. 그 합이 지금까지 구한 max보다 더 크면 최대값을 갱신하고 정답 구간도 업데이트 한다.
  • 그런데 그러다가 합이 음수가 돼 버리면... 그러면 지금까지 살펴봤던 구간은 "더 살펴볼 필요가 없고" 그냥 통째로, 아무 미련 없이 버리면 된다. 그 다음에 양수가 나오는 구간부터 시작점을 새로 설정하고 동일한 과정을 반복한다.

더 살펴볼 필요가 없기 때문에 시간 복잡도 O(n)이 가능한 것이다. 이게 아까 다수파 문제에서 점수가 음수가 돼 버린 시점에서 후보를 깔끔하게 바꿔 버리는 것과 비슷하다. 왜 이렇게 해도 되는지를 생각하는 게 이 문제의 관건이다.

Posted by 사무엘

2023/07/29 08:35 2023/07/29 08:35
, , , ,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2188

4. 숫자 자리수 잉여 구분자

21세기 들어서 프로그래밍 언어들에 알음알음 몰래 도입돼 들어간 요소 중 하나로는.. 숫자 자리수를 구분하는 잉여 구분자가 있다.

가령, Java는 _ 밑줄을 이 용도로 지원한다.
그래서 a = 1234567890이라고 쓸 것을 a = 12_3456_7890이라고 써도 되고, a = 1_234_567_890이라고 써도 된다. 소수점도 3.141_592_653 이렇게 쓸 수 있고, 0xFFFF_0000처럼 타 진법도 마찬가지이다.

C++에서는 참 흥미롭게도 '(어퍼스트로피)를 동일 용도로 지원한다. C++11에서인가 추가됐다고 한다. a=1'234'567;
_은 공백을 염두에 둔 기호인 반면, '는 콤마를 염두에 둔 기호라는 차이가 있다.

이런 구분자는 컴파일러의 입장에서는 있건 없건 토큰을 인식하는 데 아무 차이가 없다. 주석과 동급으로 전혀 필요하지 않은 잉여일 뿐이다.
단지, 사람 입장에서의 가독성을 위해서 이 구분자만은 예외로 컴파일러가 이물질이라고 토해내지 않고 무시 처리해 주는 것이다.

64비트 double 부동소수점이야 16비트 시절부터 존재했겠지만, 64비트 정수 리터럴은 쌍팔년도에는 거의 볼 수 없는 물건이었지 싶다. 이 정도로 큰 수는 10진법으로 나타내도 글자 수가 거의 20자에 달하게 된다. 그러니 필요할 때 인위로 자리수를 끊어서 표기할 수 있다면 코드의 가독성 차원에서 깨알같은 도움이 될 것이다.
다만, 학술적인 의미가 있지 않고 사람이 값을 참조할 일이 없는 단순 난수표 같은 숫자 테이블이라면 굳이 자리수 구분해서 적을 필요가 없을 듯하다.

그러고 보니 C++에는 0b로 시작하는 2진법 리터럴 표기도 추가됐다. 얘는 아무래도 길이가 굉장히 길기 때문에 8자 단위로 '로 끊어서 표기하는 게 확실히 유용하겠다.
그에 비해 C 시절부터 존재했던 8진법 표기는 진짜 아무데서도 안 쓰이는 잉여가 된 것 같다. FTP 파일 권한 777 이런 것 말고 딴 데서는 도통 본 적이 없다.

참고로 C/C++에는 줄 바꿈 문자를 없애고 토큰을 한데 이어 주는 \ 역슬래시라는 강력한 기호가 있다.
C/C++은 태생적으로 줄 바꿈에 연연하지 않고 중괄호와 세미콜론으로 문장을 구분하기 때문에 \ 가 필요할 일이 그리 많지는 않다. 사실상 #define 매크로 함수를 여러 줄에 걸쳐 길게 선언하는 용도로만 쓰인다.
하지만 그 특성상,

int a=123\
456;
const char b[]="abc\
def";

이렇게 써 줘도 얘는 a=123456이라고 인식되며, b에는 "abcdef"가 들어간다. \는 컴파일러라기보다는 거의 전처리기 수준으로 소스 코드의 두 줄을 기계적으로 연결해 준다고 생각하면 된다.

이걸로 심지어 // 주석조차 다음 줄까지 계속되게 만들 수 있으니 말 다 했다.;;
참고로, 주석은 컴파일러의 입장에서 whitespace 하나로 간주된다. 그렇기 때문에 100/*ㅋㅋㅋㅋ*/00은 100과 00을 분리시키며, 100'00과 같은 역할을 할 수 없다.

객체 지향, 제네릭/메타프로그래밍, 함수형 등 갖가지 패러다임들이 C++, C#, Java 등 메이저 언어들에 다 도입되면서 프로그래밍 언어들은 서로 비슷해지는 '수렴 진화' 중인 것 같다. 물론 자기 고유한 정체성을 상실할 정도로 완전히 똑같아지지는 않겠지만 말이다.

5. 오타

현직 프로그래머 내지 소프트웨어 엔지니어는 코딩을 한다고 해서 맨날천날 시간 복잡도, 공간 복잡도 따지고 다이나믹이니 그리디니 하는 신선놀음 같은 알고리즘 고민을 하는 게 아니다.

현실에서는 알고리즘이야 이미 만들어져 있고 잘 돌아가는 검증된 라이브러리나 오픈소스를 가져와서 쓰는 게 훨씬 더 많다.
자기가 새로운 코드를 만들어 내는 것보다 남이 만든 기존 코드를 읽고 유지보수 하고 버그를 잡는 비중이 훨씬 더 크다.
그리고 그 와중에 그나마 새로운 코드를 작성하는 게 있다면.. "뭔가 이름을 붙이는 것"의 비중이 매우 크다. 동사구이든 명사구이든..

그러니 프로그래머가 자기 조직이 마음에 안 들 때 아주 교묘하게 사보타주를 하고, 자기 후임을 엿먹이고 생산성을 저해하는 효과적인 방법이 있다.
자기가 작성하는 각종 클래스, 함수 등의 이름에다가 고의로 오타를 교묘하게 집어넣는 것이다.
아주 간단하게 getUserAdress 라든가.. receiveIncommingMessage 따위.

...;; 프로그램이야 멀쩡하게 돌아가니까 그 당시에는 아무 문제가 없는데..
문제는 나중에 그 프로그램의 버그를 잡고 기능을 추가하는 등 유지보수를 할 때다.
대놓고 약어를 쓴 것도 아니고 원래 그대로 풀어 쓴 듯한 영단어가 미묘하게 스펠링이 여기저기 틀려 있으면..
나중에 "검색"이 안 되어서 미치고 펄쩍 뛰는 일이 야기된다.

이런 코드는 여러 사람을 거쳐 가며 작업을 하기 어려우며, 처음 짰던 사람이 아니면 구조를 쉽게 파악할 수 없게 된다.
도서관에서 책을 꺼냈다가 일련번호 순서가 아닌 아무데나 꽂아 넣는 것과 같은 일이 벌어진다. (잘못 꽂힌 책은 없는 책과 같습니다)

진짜.. 개발 환경에서는 프로그래밍 언어 차원에서 코드의 문법 오류만 빨간줄을 치는 게 아니라, 명칭의 영어 스펠링 오류를 체크하는 것도 꽤 도움이 되지 싶다.

먼 옛날에 컴퓨터가 너무 비싼 물건이고 텍스트 에디터의 인터페이스가 불친절· 불편하고 디스크 공간이 부족하던 시절에는
뭐든지 getpid() 이런 식으로 짧게 줄여 쓰는 게 관행이었다. PC통신 채팅이나 전보에서 '안냐쎄여' 등으로 필사적으로 줄이는 것의 코딩 버전이나 마찬가지이다.

그러나 디스크 용량 걱정이 없어지고, 한번만 명칭을 정한 뒤부터는 에디터에서 긴 명칭을 자동 완성해 주는 기능이 매우 편리하게 발달하고(거의 90년대 말.. =_=), 또 소프트웨어의 규모가 왕창 방대해지고 공동 작업의 중요성이 커진 뒤부터는 GetProcessID() 이렇게 길게 풀어 쓰는 게 더 바람직한 관행으로 정착했다.
소스 코드가 자연어와 더 비슷해지고 길어지고 나니 스펠링 오류에 대한 취약성도 더 커진 셈이다.

6. 함수 안에 함수, 클래스 안에 클래스

파스칼 내지 Ada 같은 옛날 구시대 언어 중에서는 함수 안에 함수를 만드는 걸 지원하는 경우가 있었다. 그에 비해 C는 함수 호출 구조를 단순화시키느라 그런 걸 제공하지 않았다. 한 함수 안에서만 잠깐 쓰이는 코드 반복 패턴을 표현하려면 그냥 매크로 함수를 쓰라는 취지였던 듯하지만.. 이건 막 깔끔한 해결책은 못 됐다.

오늘날은 함수형 프로그래밍이 도입되면서 람다 덕분에 함수 안에 함수를 넣는 게 '사실상' 가능해졌다. 다만, 예전에 생각했던 그런 문법이 아니라, 함수 몸체를 지역변수에다 대입하는 굉장히 이색적인 형태로 가능해졌다는 게 신기한 점이다.

함수와 달리, 클래스는 원래부터 자기 내부에 클래스를 또 가질 수 있다. 그래서 C++은 C와 달리 계층적인 다단계 scope을 구현할 수 있으며, 필요에 따라 이거 표현을 간소화하기 위해 using이라는 키워드도 도입됐다.

함수건 클래스건.. (1) 내부에 안겨 있는 녀석의 명칭은.. 걔를 품고 있는 outer의 문맥에서만 유효하고 거기서만 접근 가능하다. 이건 너무나 당연한 이치이다.
그런데 안겨 있는 녀석은.. (2) 반대로 자기를 안고 있는 outer의 멤버(클래스의 경우)나 변수(함수의 경우)에 접근 가능해야 한다.

C/C++은 (2)를 지원하는 것이 미흡하고 인색했다. 그래서 C 시절부터 함수 안에 함수 같은 건 골치 아프니 지원하지 않았으며, 클래스 안의 클래스도 바깥 클래스의 인스턴스 멤버로 접근을 지원하지 않았다. Java로 치면 static class밖에 지원하지 않은 것과 같다.
C/C++은 포인터를 그렇게도 좋아하는 언어인데, 저것들은 정수 하나짜리 날포인터만으로 구현할 수 없는 개념이어서 지원을 안 한 것이지 싶다.

static 함수야 클래스에만 소속됐지, 클래스의 각 인스턴스에 매여 있지는 않아서 this 포인터가 존재하지 않는 함수이다(0). 사실상 전역 함수이나 마찬가지이다.
그리고 일반적으로는 자기 자신을 가리키는 this는 메모리 주소 딱 1개만 가리키는 포인터이지만.. 하지만 객체지향을 제대로 구현하려면 this의 크기가 한 칸만으로 충분하다는 고정관념을 깨야 할 것 같다. inner class라든가 다중 상속은 이런 문제를 훨씬 더 복잡하게 만들기 때문이다.

하긴, 그래서 Java에서는 C++에 없는 Outer o = (new Inner()).new Outer(); 이런 코드가 가능하다. C++에서는 new 연산자를 오버로딩 하더라도 무조건 static 형태만 되는데, Outer는 this가 자기 자신뿐만 아니라 Inner까지 사실상 두 파트로 구성되는 셈이다.
이게 가능하니 C++ 같았으면 다중 상속을 해야 했을 것도 저렇게 퉁치고, 프로그래밍을 더 작은 객체 단위로 깔끔하게 할 수 있을 것 같다.

클래스는 그렇다 치고.. 함수가 outer의 변수에 접근하는 건 요즘 C++도 '캡처'라는 기능으로 제공하기 시작했다. 원래는 '클로저'라고 부르는 개념이었지 싶은데 말이다. 이건 프로그래머/사용자의 관점에서는 아주 편리한 기능이지만, 내부적으로는 역시 함수 실행 문맥을 가리키는 포인터를 집어넣고 어쩌구 하면서 꽤 힘든 과정을 거쳐서 구현된다.

Posted by 사무엘

2023/06/30 08:35 2023/06/30 08:35
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2177

1. C++이 C보다 편리한 결정적인 요인

C++은 템플릿, 람다 등 온갖 다양한 프로그래밍 패러다임이 추가되어 C보다 훨씬 더 방대하고 복잡한 언어가 되어 있다.
그러나 C++은 맨 처음에 객체지향 언어로 시작했기 때문에 C와의 근본적인 차이는 아무래도 이와 관련된 것들이다.
본인은 C++이 C에 비해서 더 편리하고 간결하게 코딩할 수 있고 사람의 실수를 줄여 주는 제일 강력하고 중요한 요소는 다음 세 가지라고 개인적으로 생각한다.

(1) 클래스 멤버 함수 안에서 this 포인터를 생략하고 바로 자기 멤버를 참조해도 된다.
즉, C의 함수와 비교했을 때, 복잡한 구조체의 포인터인 첫째 인자는 생략 가능하다는 뜻이다. 매번 obj->member 할 필요 없이 바로 member를 쓰면 된다.

(2) 어떤 객체 변수를 선언해 주면(지역/전역) 생성자와 소멸자를 호출하는 코드가 앞뒤에 자동으로 삽입된다.
함수나 블록의 실행이 중간에 끝나더라도(return, break) 메모리를 해제하거나 파일을 닫는 코드를 거치게 하려고 지저분한 goto문을 쓰지 않아도 된다. 예외를 던질 때에도 소멸자 처리가 자동으로 된다는 건 longjmp 따위로 결코 흉내 낼 수 없는 엄청난 축복이다.

(3) 상속이라는 걸 자동으로 제공하고, 포인터 형변환 때의 상위· 하위 상속 관계를 자동으로 맞게 판단해 준다.
파생에서 기반으로 가는 건 괜찮지만, 기반에서 파생으로 가는 건 바로 안 되고 최소한 static_cast라도 해 줘야 된다.
그에 비해 C언어는 void*냐 그렇지 않느냐 하나만 판단하고, void*가 아닌 다른 모든 타입의 포인터들은 서로 남남인 타입일 뿐이다.

2. C++과 Java의 enum class

컴퓨터 프로그램에서는 숫자가 산술 연산의 대상인 수가 아니라 그냥 이산적인 식별 번호로 취급되고, 각각의 값이 서로 완전히 다른 의미를 갖는 경우가 많다. 그래서 프로그래밍 언어에서는 범용적인 정수형뿐만 아니라 sub-range 내지 열거형이란 걸 제공하곤 한다.

sub-range는 파스칼이나 Ada 같은 옛날 언어 유행으로 끝나는 분위기이고, 요즘 대세는 열거형이다.
C언어는 열거형이란 게 있긴 했지만 모종의 이유로 인해 매크로 상수가 훨씬 더 많이 쓰였다. 하긴, 그쪽은 참/거짓 bool 형조차 없었고 그냥 다 int로 퉁쳐서 썼을 정도로 int 만능 덕후 성향이 좀 있었다. =_=

C++에서는 C++11 버전부터 enum class라는 것이 도입됐다. (1) scope을 반드시 지정해 줘야 하고, (2) 정수형으로 암시적으로 형변환이 되지 않아서 type-safety가 강화되니 굉장히 적절한 변화인 것 같다.
즉, 평범한 enum이라면 int를 받는 아무 곳에서나 ENUM_VALUE라고만 써도 됐을 텐데, enum class라면 반드시 static_cast<int>( EnumClass::ENUM_VALUE ) 라고 길게 지정해 줘야 하게 된 것이다. type safety가 강화되었다.

Java에도 enum이 있긴 하지만, 후대인 Java 5에서 추가로 도입된 물건이다. 그렇기 때문에 거기도 상수 명칭을 선언하는 용도로는 재래식 static final int 뭉치가 더 많이 통용돼 왔다.
같이 도입된 건지 또 나중에 추가된 건지는 모르겠지만, Java에도 enum class라는 게 존재한다. 그런데 이건 C++과는 관점이 전혀 다른 재미있는 물건이다.

public enum Planet {
    MERCURY (3.303e+23, 2.4397e6),
    VENUS   (4.869e+24, 6.0518e6),
    EARTH   (5.976e+24, 6.37814e6),
    MARS    (6.421e+23, 3.3972e6),
    JUPITER (1.9e+27,   7.1492e7),
    SATURN  (5.688e+26, 6.0268e7),
    URANUS  (8.686e+25, 2.5559e7),
    NEPTUNE (1.024e+26, 2.4746e7);

    private final double mass;   // in kilograms
    private final double radius; // in meters
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }
   (... 이후 생략)
}

이렇게, enum {} 내부에는 명칭들을 쭈욱 쓴 뒤, 세미콜론을 찍으면 명칭 나열을 종결할 수 있다.
그 뒤, 다음부터는 클래스를 선언하는 것처럼 public이니 private니 어쩌구 하면서 멤버 함수와 멤버 변수를 쓰면 얘는 enum의 탈을 쓴 평범한 클래스가 된다.

허나, 이 enum 클래스는 new 연산자를 사용해서 임의의 인스턴스를 만들 수 없다. 이 enum의 인스턴스는 저 명칭으로 선언된 녀석들만이 허용된다. 그래서 enum 명칭을 선언과 동시에 저렇게 생성자 함수에다 전할 인자를 ()로 지정할 수 있다. 우와~~~

즉, 일반적으로 enum 명칭들은 0 1 2 3  같은 숫자의 alias에 불과한 반면, enum class는 각각의 명칭들이 이 클래스의 붙박이 인스턴스가 된다는 것이다.
enum은 상수를 나타내는 만큼, enum 클래스는 멤버들도 다들 final로 선언해서 실행 중에 값이 변경되지 않는 속성을 지정하게 하는 편이다.

enum 명칭이 하나밖에 없으면..?? 얘는 자연스럽게 이 클래스의 싱글턴/단일체가 된다. 그렇기 때문에 Java의 enum class는 싱글턴을 만드는 정석 디자인으로 통용되기도 한다.
생성자를 private로 감추는 등 별별 쑈를 해도 serialize나 reflect 같은 꼼수를 통해 싱글턴 객체를 여러 개 만드는 게 가능한 반면, enum class는 언어 차원에서 그런 일이 벌어지지 않는다는 게 보증된다.

정말 신기한 용법이다. C++의 enum class는 클래스처럼 취급되는 enum이지만, Java의 enum class는 enum처럼 생긴 클래스라고 볼 수 있겠다.

3. 이름이 붙지 않은 일회용 함수/클래스

2000년대 이후부터는 C++, C#, Java 같은 주류 프로그래밍 언어에 객체지향뿐만 아니라 함수형이라는 패러다임이 도입되었다. 덕분에 중괄호 {}로 둘러싸인 코드를 통째로 변수에 대입한다거나, 심지어 함수의 인자로 일회용으로 익명으로 전하는 게 가능해졌다. 인자를 받아서 리턴값을 주는 코드의 묶음이지만 굳이 함수의 형태로 선언· 정의하고 이름을 붙일 필요가 없다는 것이다.

심지어는 클래스까지 이렇게 간편하게 선언해서 그 인스턴스를 넘겨줄 수 있다.
Java에서 무슨 이벤트에 대한 handler나 listener를 인자로 넘겨줄 때, new XXXX { } 이러면서 객체 선언과 새 파생 클래스 선언과 주요 함수 오버라이딩을 한번에 하는 것 말이다.

그런데, 이렇게 이름 없는 함수나 이름 없는 클래스는 태생적으로 이름이 필요한 요소를 언어의 문법 차원에서 구현할 수 없다.
람다 함수는 자기 자신을 호출하는 재귀호출을 구현할 수 없다.
그리고 이름 없는 클래스는.. 정말 웃기게도 컴파일러가 기본 생성해 주는 것 말고 자신의 독자적인 생성자와 소멸자를 가질 수 없다. =_=;; 흠..

C++은 Java처럼 저렇게 함수 인자에서 새 파생 클래스를 즉석에서 만드는 것까지 지원하지는 않지만.. 새 클래스를 선언할 때 이름을 생략할 수 있다. 이건 반대로 Java에서 지원하지 않는 문법이다.
이름 없는 클래스나 함수를 만드는 게 가능하니 이름에 의존하지 않고 생성자· 소멸자나 함수 자기 자신을 지칭하는 방법이 있긴 해야 할 텐데.. 이건 그냥 언어 차원에서의 한계로 남겨 두려는가 보다.

참고로 C++은 이름 없는 namespace라는 것도 지원해서 얘는 C의 static의 상위 호환으로 간주하고 있다. 즉, 이 영역에 선언되는 함수나 변수는 다른 번역 단위에서는 인식되지 않는 private한 물건이 된다.
그 밖에 이름 없는 구조체· 공용체도 있는데, 개인적인 생각은 오프셋 보정을 위해 크기(자리)만 차지하는 용도이고 실제로 쓰이지는 않는 멤버에 대해서도 이름을 생략할 수 있었으면 좋겠다.

Posted by 사무엘

2023/06/27 08:35 2023/06/27 08:35
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2176

« Previous : 1 : 2 : 3 : 4 : 5 : ... 31 : Next »

블로그 이미지

그런즉 이제 애호박, 단호박, 늙은호박 이 셋은 항상 있으나, 그 중에 제일은 늙은호박이니라.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2024/04   »
  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        

Site Stats

Total hits:
2677531
Today:
2099
Yesterday:
2124