관계 연산자는 두 값을 비교하는 데 사용된다. 관계 연산자에는 아래와 같은 연산자가 있다.
- `==` (같다)
- `!=` (같지 않다)
- `>` (크다)
- `<` (작다)
- `>=` (크거나 같다)
- `<=` (작거나 같다)
컴퓨터가 관계 연산자를 처리하는 방법
1. 문법 분석 및 토큰화
관계 연산자가 포함된 코드를 작성하면, 컴퓨터는 먼저 문법을 분석하고 코드를 토큰화한다. 소스 코드는 코드의 요소를 나타내는 일련의 토큰으로 변환된다.
예를 들어, `a > b` 라는 표현식은 세 부분으로 토큰화된다.
- `a` (변수)
- `>` (관계 연산자)
- `b` (변수)
2. 의미 분석
토큰화가 완료된 후, 컴파일러나 인터프리터는 의미 분석을 수행하여 연산이 유효한지 확인한다. 여기에는 `a`와 `b`의 타입이 `>` 연산자와 호환되는지 확인하는 작업이 포함된다. 예를 들어, `a`와 `b`는 일반적으로 숫자 타입이거나 비교 가능한 타입이어야 한다.
3. 중간 표현
그 다음 단계는 표현식을 중간 표현(IR*)으로 변환하는 것이다. 이는 실제 하드웨어보다 추상화된 낮은 수준의 코드이다. 예를 들어, `a > b`는 `CMP a, b`와 같은 중간 언어 명령어로 번역될 수 있다.
*IR: Intermediate Representation. 컴파일러가 소스 코드를 기계어로 번역하는 과정에서 사용하는 중간 단계의 표현 형식. IR은 컴파일러가 다양한 최적화와 코드 생성 작업을 수행할 수 있게 하는 중요한 역할을 한다.
4. 코드 생성
중간 표현은 기계어로 번역된다. 기계어는 CPU의 아키텍처에 따라 다르지만 비교를 처리하는 특정 명령어가 포함된다.
예를 들어, x86 어셈블리 언어에서:
- `CMP` 명령어는 두 값을 비교하는 데 사용된다.
- `CMP` 명령어 다음에는 `JG` (크면 점프), `JL` (작으면 점프), `JE` (같으면 점프) 등과 같은 조건부 점프 명령어가 비교 결과에 따라 사용된다.
예시:
CMP eax, ebx ; eax와 ebx 레지스터의 값을 비교
JG label ; eax가 ebx보다 크면 label로 점프
5. 실행
실행 중에 CPU는 다음 단계를 수행한다:
1. 피연산자 가져오기: `a`와 `b`의 값을 레지스터나 메모리에서 가져온다.
2. 비교 연산: CPU는 산술 논리 연산 장치(ALU)를 사용하여 비교를 수행한다. ALU는 비교 결과에 따라 프로세서의 상태 레지스터에 특정 플래그를 설정한다(예: 제로 플래그, 부호 플래그, 오버플로 플래그).
3. 플래그 설정:
- 제로 플래그(ZF): 비교 결과가 0일 때(예: `a == b`)
- 부호 플래그(SF): 결과가 음수일 때(예: 정수 비교에서 `a < b`)
- 캐리 플래그(CF): 부호 없는 비교일 때
- 오버플로 플래그(OF): 정수 비교에서 오버플로가 발생했을 때
Let’s learn about `CMP` instruction
`CMP` 명령어는 두 피연산자를 비교하는 데 사용된다. 이 명령어는 피연산자를 뺄셈하여 플래그 레지스터의 값을 설정하지만, 실제 피연산자의 값은 변경하지 않는다. 이를 통해 조건부 분기 명령어와 함께 사용되어 프로그램의 흐름을 제어할 수 있다.
CMP 명령어의 사용법
`CMP` 명령어의 일반적인 형식은 다음과 같다:
CMP destination, source
여기서 `destination`과 `source`는 비교할 두 피연산자다. `destination`에서 `source`를 빼고 그 결과를 버린다. 대신, 상태 플래그 레지스터의 플래그가 설정된다.
플래그 레지스터
`CMP` 명령어는 상태 플래그 레지스터의 여러 플래그를 설정한다. 주요 플래그는 다음과 같다:
- ZF (Zero Flag): `destination - source`의 결과가 0일 경우
- SF (Sign Flag): `destination - source`의 결과가 음수일 경우
- CF (Carry Flag): `destination`이 `source`보다 작을 경우(부호 없는 비교)
- OF (Overflow Flag): 부호 있는 연산에서 오버플로가 발생할 경우
6. 분기 (해당되는 경우)
조건부 연산(예: `if (a > b) { ... }`)의 경우 CPU는 비교에 의해 설정된 플래그를 사용하여 코드의 다른 부분으로 분기할지 여부를 결정한다.
예를 들어:
CMP eax, ebx ; eax와 ebx를 비교
JG greater_label ; 크면 greater_label로 점프
7. 결과 반환
비분기 컨텍스트에서(예: `result = a > b`와 같은 표현식), 비교 결과는 일반적으로 불리언 값(`true` 또는 `false`)으로 레지스터나 메모리 위치에 저장된다.
고수준 언어의 예시
다음 C++ 코드를 고려해 보자:
int a = 5, b = 3;
bool result = a > b;
1. 구문 분석: 컴파일러가 `a > b` 표현식 분석
2. 중간 코드: 중간 코드로 번역
t1 = 5
t2 = 3
t3 = t1 > t2
3. 기계어: 중간 코드는 기계어로 번역
MOV eax, 5 ; eax에 5를 로드
MOV ebx, 3 ; ebx에 3을 로드
CMP eax, ebx ; eax와 ebx를 비교
SETG al ; 크면 al 설정
4. 실행: CPU는 이 명령어들을 실행 및 비교 수행. 결과를 지정된 레지스터나 메모리에 저장
이 단계를 통해 컴퓨터가 관계 연산자를 처리하는 복잡성과 효율성을 이해할 수 있으며, 하드웨어 수준에서 정확하고 효율적인 비교를 보장한다.
그럼 <, <=, >, >= 중 어떤 연산자가 가장 효율적일까?
모든 연산자가 플래그 레지스터를 검사하는 방식이 다르지만, 실질적인 연산 횟수나 복잡도 측면에서 큰 차이는 없다. 요즘 CPU의 성능 최적화 기술 덕분에 이러한 미세한 차이는 성능에 거의 영향을 미치지 않는다.
결론적으로 코드의 논리적 흐름과 가독성을 유지하는 것이 더 중요하다. 필요한 연산자를 사용하여 코드의 의도를 명확히 표현하는 것이 좋다.
각 연산자의 플래그 검사
- `<` 연산자 (`JL` - Jump if Less)
- 플래그 검사: `SF != OF`
- `<=` 연산자 (`JLE` - Jump if Less or Equal)
- 플래그 검사: `ZF == 1 or SF != OF`
- `>` 연산자 (JG - Jump if Greater)
- 플래그 검사: `ZF == 0 and SF == OF`
- `>=` 연산자 (JGE - Jump if Greater or Equal)
- 플래그 검사: `SF == OF`
*참고: http://unixwiz.net/techtips/x86-jumps.html
결론
어떤 연산자를 써도 성능 차이가 미미하지만 플래그 검사 측면에서 가장 적은 비교를 수행하는 연산자는 `<` 연산자 (`JL`)와 `>=` 연산자 (`JGE`)다.