2.2 바인딩(Binding)과 자료형 검사(Type Checking)
Binding
Binding : Attribute의 값을 결정하는 행위
Binding : Entity와 Attribute 또는 Operation과 Symbol 간의 관계를 결정하는 행위이다.
ex) C언어에서 Type Binding은 언제 일어나는가? : Compile-Time
ex) C언어에서 Value Binding은 언제 일어나는가? : Run-Time
~> Attribute에 따라 Binding 시점은 다를 수 있다.
Binding Time
-
Language Design Time : 특정 연산자가 '특정 연산'을 의미하도록 결정하는 것은 언어 설계 시간이 결정된다. 아스타리스크(*)라는 Symbol이 곱셈 Operation을 의미한다는 것은 언어 설계 타임에 결정된다.
- 말장난같지만, 실제로 그렇다.
-
Language Implementation Time : Integer와 같은 Data Type이 Possible Value의 범위(Attribute)를 결정하는 것은 언어 구현 시간에 결정된다.
- int는 Word Size로, CPU가 16비트냐, 32비트냐에 따라 다르다.
- 언어에 대한 컴파일러를 구현할 때, 타겟 하드웨어가 무엇이냐에 따라 int형의 가능 Value 범위를 결정한다. 따라서 '언어 구현 타임'에 결정된다고 하는 것이다.
- int는 Word Size로, CPU가 16비트냐, 32비트냐에 따라 다르다.
-
Compile Time : Static Type Binding의 경우, Type(Attribute) Binding을 Compile Time에 수행한다. C언어나 Pascal이 대표적이다.
-
Link Time : (라이브러리의) Subprogram을 실제 Subprogram Code로 Binding하는 것은 링커가 수행되는 링크 타임에 이뤄진다.
- 컴파일러는 .c 파일을 .o/.obj 파일로 만드는 것이다. (C기준)
- 이때, C언어는 파일 단위로 컴파일한다. 우리는 예를 들어 test1.c, test2.c라는 파일을 함께 묶어서 gcc로 컴파일해 하나의 exe파일을 만들어본 경험이 있다.
- 이렇게 복수의 파일을 연결해서 하나의 최종 파일을 만들어주는 과정을 Linking이라 한다.
- Linking은 라이브러리 링킹을 포함한다.
- 참고로, 링킹은 실제 코드를 가져다 붙이는 것은 아니다. 실제 코드가 있는 위치(주소)를 알아내고, Jump 명령을 이용해 거기로 움직이는 형태이다.
ex) 예를 들어 다음과 같이 두 개의 파일이 있다고 해보자.
- main.c
int i, j;
int main(void) {
sub();
return 0;
}
- sub.c
int k, i; // same name for two global variables
void sub(void) {
...
}
~> 두 .c 파일을 컴파일하면, 컴파일러는 파일 단위로 독립적으로 컴파일을 수행하기 때문에 두 코드 모두 "함께" 정상적으로 컴파일된다.
~> 이어서 링킹이 이뤄지는데, 두 파일에 같은 name을 지니는 변수 i에 대해서, 서로 다른 변수로 취급한다.
=> 왜냐? 컴파일이 이뤄지고 나면 '변수의 name'은 의미가 없어진다. 컴파일로 만들어진 .o/.obj 파일에는 변수명 대신 레지스터명이 쓰이기 때문이다.
~~> 또한, .obj파일에서 주소 어드레싱은 Offset을 가지고 수행한다(어셈블리어 코드를 생각하라).
.obj 파일을 Relocatable Code라고 한다. Loader에 의한 Loading Time에 실제 메모리 주소 할당이 수행될 것이기 때문이다.
==> 즉, 결론적으로 위의 예시는 문제없이 컴파일되고 링킹된다. 물론 바람직하진 않을 것이다. 하나의 공통된 전역변수 i를 사용하고 싶으면 extern 키워드를 사용하자.
- Load Time : 로더가 프로그램을 메인메모리에 적재할 때, 변수들에 대해 '실제 메모리 셀 주소(Physical Memory Address)' 할당이 이루어진다. 이 역시 Binding에 해당하고, Load Time Binding이다.
Variable may be bound to a storage cell when the program is loaded into the main memory.
※ .obj 파일에서의 '주소'는 오프셋을 따진다. 이 오브젝트 코드가 로딩되면, 실제 메모리 주소와 맵핑이 이루어지는 것이다.
~> 한 번 컴파일되면, 오브젝트 코드는 동일한데, 로딩 과정에서의 맵핑이 매 수행때마다 다르다. 그래서 우리가 프로그램을 실행할 때마다 메모리 주소가 다른 것이다.
~~> 빈 공간에서 적절한 공간을 따져 로더가 알아서 할당해준다.
-
Run Time
-
프로시저 내의 변수들은 해당 프로시저가 실제로 호출되었을 때, 동적으로 Storage Cell에 Binding된다. 즉, 함수의 로컬변수들은 Run-Time에 실제 메모리 주소와 맵핑된다고 볼 수 있다. (Local Variable의 Lifetime관련! 하단 서술)
-
Dynamic Type Binding : Python의 Dynamic Type Binding 역시 Run Time Binding에 해당한다. 변수 대입 연산 시미다 타입 바인딩이 이뤄진다.
-
※ Static Type Binding과 Dynamic Type Binding의 구분 기준은 Type Binding이 Run-Time 이전에 이루어지냐, 이후에 이루어지냐이다.
-> Static : 'Run-Time 이전 Binding', 그리고 '한 번 Binding되면 수행 중에 변하지 않음'을 의미한다.
-> Dynamic : 'Run-Time 동안 Binding', 또는 'Binding 후, 수행 도중 변화할 수 있음'을 의미한다.
Type Binding
Static Type Binding (Variable Declaration)
- Explicit Declaration (명시적 선언) : 프로그램에서 명령을 통해 변수의 Name과 그들의 특정 Type을 나열(list)하여 명시한다.
- 1960년대 중반에 만들어진 대부분의 프로그래밍 언어들은 이러한 명시적 선언 식의 정적 타입 바인딩을 채택한다. C언어가 대표적이다.
- 예를 들어, C에서 int a, b;라 선언하고 a = 5; 등의 연산을 진행한다.
- Implicit Declaration (암묵적 선언) : 명시적으로 선언하지 않고, '기본적인 규칙(Default Convention)'을 두어 변수의 Type을 암묵적으로 규정하는 방식이다. 이 역시 정적 타입 바인딩에 해당한다.
- FORTRAN, BASIC, PL/I 등이 해당한다.
- 예를 들어, FORTRAN에서 I, J, K, L, M, N이란 변수명은 정수형을 의미한다.
Dynamic Type Binding
- 할당 연산으로 Value가 변수에 할당될 때 Type이 결정되는 방식이다.
- 기본적으로는 Interpreter를 사용한다. 기계어를 동적으로 변화시키기는 어렵기 때문이다.
- 대표적으로 JavaScript, Python, Ruby, PHP, C# 등이 있다.
- 장점
- Flexibity가 있다. 즉, Generic하다. 범용적이라는 것이다. 하나의 프로그램으로 여러 데이터를 처리할 수 있다.
- 단점
- Compile Time에 일어난 Error는 찾기 힘들다.
- Run Time에 Type Checkig과 Space Checking 등이 이뤄지므로 효율과 수행 속도가 좋지 않다.
- Type Inference 방식 : Java나 ML에서 사용하는 방식으로, 컴파일러가 Type을 추론하는 Binding하는 방식이다.
※ Dynamic Type Binding을 채택한 프로그래밍 언어들은 Typeless한 것이 아니다!!
~> Type은 있는데, 이들이 Run-Time에 지정된다는 것이다.
Typeless Language는 Assembly Language이다.
※ Typeless Langauge : 말 그대로 자료형이 없는 프로그래밍 언어로, 통념상 Python과 같은 ‘Dynamic Type Binding’이 수행되는 언어들을 의미할 것 같지만, 사실은 이들은 모두 ‘Typed Language’이고, ‘Typeless Language’란 ‘Assembly Language’와 같이, 순수하게 ‘자료형이 없는’ 언어를 칭한다. C언어의 경우, ‘Weakly Typed’라고 보는 것이 적합하다. ‘Typeless Language’는 기본적으로 자료형이 없기 때문에 변수가 자유롭다. 변수의 값 할당에 특별한 제약이 없다. 이는 Flexible함을 의미하지만, 동시에 Reliability의 저하를 의미한다. Typed 언어에서 Type Checking을 통해 발견할 수 있던 다양한 Error를 감지할 수 없는 것이다.
Storage Binding (Lifetime)
프로그램 변수의 Lifetime이란, 해당 변수가 특정한 메모리 공간에 Binding된 것이 지속되는 시간을 의미한다.
int a;
void main(void) {
int b;
...
}
void sub(void) {
int c;
}
-
이 소스 코드를 컴파일하면, 생성된 프로그램이 Code부와 Data부, Stack부 등으로 나뉘어 메인 메모리에 적재된다. 이때, Data부분에는 a가 4바이트 공간을 차지한다(전역변수).
-
Stack에는 b와 c가 담겨야할텐데, 이 중 sub함수 내의 변수 c는 몇 바이트를 차지할까?
-
c라는 로컬 변수는 sub()이 호출될 때마다 필요한데, sub()이 몇 번 호출될지는 Run Time 이전에는 알 수 없다. 특히나 Recursion의 경우, 그 수행 횟수를 컴파일 타임에는 예측하기 어렵다.
-
그래서 일반적으로 서브루틴의 로컬 변수는 해당 프로시저가 호출될 때마다 프로그램의 Stack부분에 메모리 할당이 Dynamic하게 이루어진다.
- 위에서 Run Time Binding의 예시로 로컬 변수의 메모리 공간 맵핑이 거론된 이유가 바로 이러한 이유이다.
-
만약, 위의 예시 코드에서 sub 함수가 재귀적으로 총 3번 호출된다고 해보자. Stack에 c_1, c_2, c_3가 차례로 할당(Push)된다.
- 이때, c_1부터 c_3가 차례로 재귀호출되어 할당된 것이라 하면, 가장 먼저 없어져야하는 메모리 공간은 c_3이다. 재귀적인 순서로 보았을 때 말이다.
- 그래서 우리가 이러한 메모리 공간을 Stack부분이라고 부르는 것이다.
- 이때, c_1부터 c_3가 차례로 재귀호출되어 할당된 것이라 하면, 가장 먼저 없어져야하는 메모리 공간은 c_3이다. 재귀적인 순서로 보았을 때 말이다.
-
-
Variable의 Lifetime이란, Storage Cell(Memory) Binding이 유지되는 시간이다.
Static Variable (Load-Time)
프로그램 수행 시작 전에 메모리 셀에 변수가 바운드되고, 수행이 종료될 때까지 그 바인딩이 유지되는 변수를 'Static Variable'이라 한다.
-
Global Variable, History Sensitive Variable이 Static Variable에 해당한다.
-
장점
- Efficiency : 아무래도, Load-Time에, 메모리 적재할 때 바로 메모리 바인딩이 이뤄지고, 이후엔 바인딩이 필요 없으니 수행 속도면에서 우수하다.
-
단점
- Reduced Flexibility : Static Variable은 Flexibility가 낮다. 전역 변수만 사용해서 프로그램을 짜야한다고 해보자. 얼마나 불편하겠는가. 특히나 재귀함수를 구현하기가 어렵다.
Stack Dynamic Variable (Run-Time)
Stack Dynamic Variable이란, Type Binding은 Static하게 Run-Time 이전에 이미 결정되었지만, Storage Binding은 해당 변수가 Run-Time에서 선언되었을 때 실시간으로 이뤄지는 변수를 의미한다.
-
Local Variable (in Procedure)
- 상기한 예시 상황이 바로 이러한 Run-Time Storage Binding이다.
-
나중에 호출된 것이 먼저 제거되는 재귀적 흐름을 지원하기 위해 Stack 자료구조의 논리를 따르는 Stack이라는 메모리 공간을 마련해 관리한다.
- 그래서 Stack Dynamic Variable(Local Variable)의 Lifetime이 해당 변수를 선언한 Procedure의 호출부터 종료까지인 것이다.
-
장점
- 재귀(Recursion)를 지원한다.
- 서로 다른 프로시저 간의 같은 메모리 셀 공유가 가능하다. (Sharing of the Same Memory Cells)
- 일전에 사용(변수를 할당)했던 Stack 메모리 공간을 다시 사용할 수 있다는 말이다.
- 단점
- 로컬 변수들에 대한 메모리 셀을 할당하고 해제하는 과정에서 Run-Time Overhead가 (상대적으로 Static Variable에 비해) 많다.
※ Stack Overflow : 우리가 프로그래밍 중 스택 오버플로우를 경험하는 이유는, 위와 같은 Stack 공간에 메모리 할당을 무리하게 많이, 급격하게 많이 하게 되면 일어난다. 예를 들어, 잘못된 재귀 흐름을 가진 알고리즘을 짜면, 금새 Stack 공간이 초과되는 것이다. Factorial을 재귀적으로 구하고, 한 번 호출마다 크기 10,000짜리 배열을 같이 할당하는 이상한 함수가 있다고 하면, 이는 금새 Stack 부분을 터트릴 것이다.
※ Stack Dynamic Variable의 Run-Time Storage Binding 개념은, Heap 공간에 메모리를 할당하는 '동적 할당(바로 아래에 서술)'과는 엄연히 다른 개념이다. 동적할당은 사용자가 메모리를 직접 할당/해제한다. 즉, 메모리의 할당/해제 순서가 스택의 논리와는 전혀 관련이 없다. 동시에, 할당되는 공간도 다르다.
C언어는, Local Variable의 Storage Binding은 Run-Time에, Static Variable의 Storage Binding은 Load-Time에, 그리고 모든 Type Binding은 Static하게 Compile-Time에 이뤄진다.
Question) Recursion 구현 시, 혹시 함수에 선언할 수 있는 변수의 개수가 한정되어있는가?
Answer) Nope. 전혀 상관없다. 하지만, 재귀호출 시 터지지 않게 잘 고려해서 선언해야겠지?
Recursion은 사실 '수행하는 일에 비해서 Overhead가 너무나 많은 매커니즘'이다. 보기엔 좋지만, 함수 호출/반환 명령이 과도하게 사용되고, 이들은 프로그램 수행 속도를 급격하게 낮추는 요소이다.
Explicit Heap Dynamic Variable (Run-Time)
Explicit Heap Dynamic Variable : 프로그래머가 직접 '명시적인 Run-Time 명령'을 통해 Nameless한 메모리 공간을 할당/해제하여 만들 수 있는 변수를 의미한다.
Nameless한 Object를 포인터변수 따위로 가리키는 것이다. (C의 동적할당을 생각!)
-
즉, 사용자가 직접 명시적으로 런타임에 메모리를 할당하고 해제할 수 있는 변수를 의미한다. C언어의 동적할당으로 만든 변수가 바로 여기에 해당한다.
-
Stack Dynamic Variable과 마찬가지로, Type Binding은 Compile Time에 이뤄지고, Storage Binding이 Run-Time에 일어나는 것이다. (C 기준)
- 차이는, Lifetime이 다른 것이다. 동적할당은 사용자 마음대로이고, 로컬 변수는 함수의 시작-끝인 것!
-
예시
- Pascal : procedure
- Ada : operator
- C : malloc/free function, C++ : new/delete
-
장점
- 동적인 자료구조를 구축할 수 있다. Linked List나 Tree같은 것 말이다.
-
단점
- Garbage(하단 서술)를 완벽히 피하는 코드를 짜기가 어렵다. 또한, Segmentation Fault가 발생하기 쉽다. 즉, 완벽하고 세심하게 다루기가 어렵다. (Unreliable)
- 메모리 할당, 참조, 해제 과정에서 Cost가 많이 든다. (Inefficient)
void sub(int n) {
int *a;
if (n < 1) return;
a = (int*)malloc(...);
sub(n - 1);
}
- 위와 같은 서브루틴에 n=100을 넣어 호출한다고 해보자. 재귀적인 흐름으로 총 100번 호출될 것이다.
- Stack 공간에는 100개의 int형 포인터변수가 Push될 것이다.
- n < 1인 순간부터는, 최초 sub 호출 전 Top 위치까지 Stack이 쭉 Pop될 것이다.
- 반면, Heap 공간에 동적할당된 영역들은 그대로 남아있게 된다. 따로 free하지 않았기 때문이다.
- 이러한, 동적으로 할당되놓고 해제되지 않은 메모리 영역을 'Garbage'라고 한다. 이들은 Memory Leakage를 야기할 수 있다.
- 그래서 Garbage Collection이 중요하다!
- 이러한, 동적으로 할당되놓고 해제되지 않은 메모리 영역을 'Garbage'라고 한다. 이들은 Memory Leakage를 야기할 수 있다.
- Stack 공간에는 100개의 int형 포인터변수가 Push될 것이다.
Explicit Heap Dynamic Variable을 사용할 때는 이러한 Garbage가 쌓이는 것을 조심해야한다.
~> 단순히 응용 프로그램만 그런 것이 아니라, OS와 같은 시스템 프로그램도 마찬가지로 Garbage를 조심해야한다. Garbage Collector가 있긴 하지만, 그 기능이 아직까지도 '완벽'하진 않기 때문에, 프로그래머가 항상 Garbage를 조심하는 방식으로 프로그래밍하는 것이 매우 중요하다.
type intnode = ^integer;
var anode : intnode;
...
new(anode);
...
dispose(anode);
...
~> 동적할당 후 해제하는 Pascal Type Pseudocode
Implicit Heap Dynamic Variable (Run-Time)
Implicit Heap Dynamic Variable : Variable에 Value Assignment가 이뤄질 때마다 Heap 메모리 공간으로의 Storage Binding이 일어나는 방식이다.
Explicit Heap Dynamic Variable과 Implicit Heap Dynamic Variable은 모두 Heap 공간에 메모리를 할당하지만, 차이점은, EHDV은 Programmer가 할당/해제를, IHDV은 System이 할당/해제를 수행한다는 점이다.
-
Dynamic Type Binding 방식의 프로그래밍 언어에서 채택하는 방식이다. Type Binding과 Storage Binding 모두 Run-Time에, 할당된 Value에 따라 이뤄지는 것이다.
-
Python, JavaScript, APL 등
LIST = 11.5 2.7 -6.3 // Memory Allocation for floats
LIST = 33 29 128 // Memory Allocation for integers
~> 수행 중 할당된 값을 확인해 실시간으로 Type과 Storage Binding을 수행한다.
-
장점
- 융통성이 높다. (High Flexibility)
-
단점
- Run-Time에 발생하는 Overhead가 많다. 동적할당/해제 과정에서 오버헤드가 발생한다.
- Error Detection이 힘들다.
※ Dynamic Type Binding 언어라 할지라도, 메모리 공간 구분 시 Stack 부분이 있다. Stack 부분에 포인터 변수를 두고, 그 포인터가 Heap 공간의 Nameless Object 공간을 동적으로 할당한 후, 해당 공간을 가리키는 원리인 것이다.
~> 그렇다. 동적할당과 완전히 같은 원리인 것이다. 단지, 프로그래머의 의도에 따라 이뤄지느냐, 시스템이 알아서 해주냐에 따라 EHDV과 IHDV가 갈리는 것일 뿐이다.
※ 지난 포스팅에서도 언급한 것처럼, Dynamic Type Binding 언어는 Typeless Language가 아니다. Typed 언어인데, 단지 Type Binding이 Run-Time에 Dynamic하게, Implicit하게 이뤄지는 것일 뿐이다.
※ 우리가 극단적으로 수행 속도만 중요시한다면, 사실 Static Variable이 최고인 것이다. 물론, 그런 관점에서만 프로그래밍할 순 없지만 말이다.
Type Checking
- Type Checking은 Operator의 Operand가 Compatible Type을 가지고 있는지 검사하는 작업이다.
-
이때, 'Compatible Type'이란, 연산자(Operator)에 해당 Type의 사용이 적합한지 여부를 말한다. 즉, 'Legal Type for the Operator'인지 확인하는 것이다.
- Compiler가 Illegal Type에 Implicit한 Coercion을 수행한 결과가 적합한지 따지는 것까지 포함한다.
-
- 즉, Operator의 Operand의 Type, 또는 Operand의 Coercion된 Type이 Operator에 적합한지, 언어의 규칙을 위반하지 않는지 검사하는 것이다.
-
Static Type Binding이 이뤄지는 언어에서는 Type Checking도 마찬가지로 Static Type Checking으로 수행한다.
-
Dynamic Type Binding이 이뤄지는 언어의 경우 Type Checking은 꽤나 복잡해진다.
- Pascal의 Variant Record나, Python, JavaScript와 같은 언어들!
- 매 수행마다 똑같은 메모리 공간에 서로 다른 Type의 Value를 저장하고 바꾸고 할 수 있는 그런 언어들! 그런 언어들의 Type Checking은 어렵다는 것!
Strongly Typed Language
Compile-Time에 모든 Type Error를 감지하는 프로그래밍 언어를 'Strongly Typed Language'라고 한다.
~> 즉, 사용자 소스 코드에 있는 타입 에러를 미리, 다 확인한다는 것이다.
-
Static Type Binding 언어여야 이러한 Strongly Typed 속성을 가지기가 쉽다.
-
어떠한 유형의 Type Error도 다 감지할 수 있어야 'Strongly Typed'이다.
-
Not Strongly Typed Language
- C, C++, FORTRAN 77
- Actual Parameter와 Formal Parameter 사이의 Type Checking이 이뤄지지 않는다.
- 쉽게 말해서, 함수 파라미터 패싱할 때 타입 체킹이 이뤄지지 않는다는 것이다.
- printf 함수에 포맷 문자로 %d를 두고 'a'라는 문자를 넘겨도, 전혀 문제가 되지 않는다.
- 현대의 C 컴파일러에서는 Prototyping(Function Declaration) 기능을 지원해 이러한 문제를 방지하고자 한다.
- C, C++, FORTRAN 77
-
Nearly Strongly Typed Language
-
Pascal : Variant Record 개념을 제외하고는 강하게 Typed된 언어이다.
-
Ada : 에이다 언어는 미국 국방성에서 무기 개발 전용으로 사용하는 프로그래밍 언어이다. 무기 개발을 위해선 '정밀함'이 최우선이기에 이러한 'Strongly Typed' 속성이 있는 것이다.
-
Type Compatible & Equivalence
-
Type Compatible : 두 변수 Type이 있을 때, 한 쪽의 Value를 다른 쪽에도 할당할 수 있을 때, 이를 'Type Compatible'하다고 부른다.
-
Type Equivalence : 두 Type이 Coercion없이 대체 가능할 때를 말한다.
-
Name Type Equivalence : 말 그대로, 두 Type의 Type명을 보고 판단. 엄격하다. Naive하게 Type 이름을 비교해, 다르면 다른 Type이고 Equivalent하지 않은 것이다.
-
Structure Type Equivalence : Type A와 Type B가 있는데, Type A의 Value Range가 Type B의 Value Range 안에 포함되면, 이는 구조적으로 서로 Compatible하다고 보는 방식
-
Author And Source
이 문제에 관하여(2.2 바인딩(Binding)과 자료형 검사(Type Checking)), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@junttang/PL-2.2-바인딩Binding이란저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)