[Go] 내 맘대로 계산기 프로그램 만들기 [2] - 사칙연산 및 소요시간 테스트
사칙연산이 가능해야 한다.
예외처리가 확실해야 한다(Divide by Zero, Overflow, FPE, input 예외 처리).
연산식에 대한 연산 시간을 임의의 기준으로 사전 평가하여 시간이 오래걸린다고 판단되면 백그라운드에서 진행하게끔 한다.
위 화면에서, 1 번을 누르면 정수에 대한 사칙연산이 가능한 화면으로 넘어갈 것이다.
일단 정수에 대한 사칙연산만 생각해보자.
필요한 함수를 생각해보자.
1. 사칙연산에 필요한 연산식 입력받는 화면 출력 함수
1-1. 예외처리: 입력 형식 지키게끔 하기
2. 연산식(string)을 중위 표기식에서 후위 표기식으로 변환해주는 함수(괄호도 처리 가능해야 한다)
2-1. 하나의 연산식에 대한 속도 평가 함수(추후 병렬처리 기준을 세울 결과를 산출)
2-2. 예외처리: Overflow 방지
3. 연산 진행 및 결과 산출 함수
3. 예외처리
4. 메인 화면으로 복귀
4. 예외처리
구현
입력받은 연산식(중위 표기식)을 후위 표기식으로 변환하는 함수
스택과 조건 분기를 사용하면 간단히 구현 가능하다.
func conversion(input string) string {
var result strings.Builder
stack := []byte{}
for i := 0; i < len(input); i++ {
v := input[i]
if isOperator(byte(v)) {
if byte(v) == '(' {
stack = append(stack, byte(v))
result.WriteByte(' ')
} else if byte(v) == ')' {
for len(stack) > 0 && stack[len(stack)-1] != '(' {
result.WriteByte(stack[len(stack)-1])
result.WriteByte(' ')
stack = stack[:len(stack)-1]
}
stack = stack[:len(stack)-1] // '(' 도 pop
} else {
// 3. stack의 top이 2의 연산자보다 우선순위가 낮을 때까지 pop, 이후 자신을 담음
cur_priority := operatorPriority(byte(v))
for len(stack) > 0 && cur_priority <= operatorPriority(stack[len(stack)-1]) {
result.WriteByte(stack[len(stack)-1])
result.WriteByte(' ')
stack = stack[:len(stack)-1]
}
stack = append(stack, byte(v))
}
} else if isNum(byte(v)) {
for i < len(input) {
if !isNum(input[i]) {
break
}
result.WriteByte(input[i])
i++
}
result.WriteByte(' ') // 피연산자 구분을 위해 공백 추가
i--
}
}
for len(stack) > 0 {
result.WriteByte(stack[len(stack)-1])
result.WriteByte(' ')
stack = stack[:len(stack)-1]
}
return result.String()
}
알게된 점
Go는 표준 스택 자료구조가 존재하지 않는다.
슬라이스로 간단하게 구현할 수 있다.
integer stack을 만들고 싶다면,
integer 슬라이스를 만들고 초기화 하면 된다.
Stack := []int{}
stack의 ADT는 아래처럼 구현 가능하다.
- Push
- 내장 함수 append( [ ] T, elem) 으로 구현 가능하다.
- Pop
- Pop 한 원소를 관리(e.g. 출력)해야 한다면, 슬라이스의 마지막 인자를 다른 변수에 저장한 후
- 아래처럼 ' : '를 사용해 새로운 슬라이스를 만들어 기존 슬라이스에 할당한다.
- 아래처럼 [ : ] 하면 새로운 슬라이스가 만들어지기 때문에, 기존 슬라이스에 재할당해야한다.
- 이러면 우측 연산식에서 참조한 stack은 바로 삭제되는건 아니고 나중에 go gc가 회수한다.
stack = stack[:len(stack)-1]
후위 표기식 계산 함수
func Evaluate(postfix string) int {
stack := []int{}
tokens := strings.Fields(postfix)
for _, token := range tokens {
token_byte := ([]byte(token))[0]
if isOperator(token_byte) { // 연산자인 경우 피연산자 두 개를 꺼내서 연산 진행
num1 := stack[len(stack)-1]
num2 := stack[len(stack)-2]
stack = stack[:len(stack)-2]
res := operation(num1, num2, token_byte)
stack = append(stack, res)
} else { // 피연산자인 경우 스택에 추가
num, _ := strconv.Atoi(token)
stack = append(stack, num)
}
}
return stack[0]
}
소요 시간 테스트 함수(goroutine이 main 한 개 일 때)
func MeasureExecutionTime(f func(string) int, postfix string) int {
start := time.Now() // 함수 실행 시작 시간
start_goroutines := runtime.NumGoroutine() // 시작 시점의 Goroutine 수
// 함수 실행
res := f(postfix)
fmt.Println("\n\n")
elapsed := time.Since(start).Milliseconds()
// r := 3475393 / 234 * 1242312 * 1241235 / 12356123
// fmt.Print(r, "\n")
// 종료 후 경과 시간 계산
end_goroutines := runtime.NumGoroutine() // 종료 시점의 Goroutine 수
numCPU := runtime.NumCPU() // CPU 수
numThreads := runtime.GOMAXPROCS(0) // 사용 중인 최대 스레드 수
// 함수 이름 출력
funcValue := reflect.ValueOf(f)
if funcValue.Kind() == reflect.Func {
pc := runtime.FuncForPC(funcValue.Pointer())
fmt.Printf("Function name: %s\n", pc.Name()) // 함수 이름 출력
}
// 결과 출력
fmt.Printf("Execution time: %d ms\n", elapsed)
fmt.Printf("Start Goroutines: %d, End Goroutines: %d\n", start_goroutines, end_goroutines)
fmt.Printf("Available CPUs: %d, Max Threads (GOMAXPROCS): %d\n", numCPU, numThreads)
return res
}
함수 f 를 실행하는데 걸린 소요 시간을 측정하고
사용된 goroutine의 수도 측정하고
가용가능한 CPU수와 해당 프로세스를 실행하며 사용한 Max Threads 수도 출력한다
(내 PC의 논리 CPU 수(즉 스레드 수)는 12개다)
Go Test 및 Benchmark 모듈
테스트 함수를 직접 구현하다,
문득 Go에서 자체적으로 지원하는 test 및 benchmark 모듈이 없나 궁금해서 찾아봤습니다.
당연히 있었네요!
다음 포스트에서 한 번 알아봐야겠습니다.
배운 점
같은 패키지에 속한 각 go 파일에서 서로의 함수를 참조하고 싶다면 함수의 이름을 대문자로 시작하게 해야 한다.
4. 실제 테스트의 필요성
이론적으로는 10개 이상의 연산부터 고루틴이 유효할 수 있지만, 실제 성능은 많은 변수에 따라 달라지므로 벤치마크 테스트가 필수적입니다. Go에서 testing 패키지를 사용하여 동일한 입력에 대해 고루틴을 사용하는 경우와 그렇지 않은 경우의 실행 시간을 비교하는 벤치마크를 실행해보면 가장 정확한 판단을 내릴 수 있습니다.
요약:
- 연속적인 10개 이상의 곱셈/나눗셈 연산이 있을 때 고루틴을 사용하는 것을 고려할 수 있습니다.
- 시스템이 여러 CPU 코어를 가지고 있을 때 효과가 더 커집니다.
- 연산이 복잡할수록 고루틴 사용의 효과가 더 두드러집니다.
- 벤치마크 테스트를 통해 실제 성능을 평가하는 것이 중요합니다.
고루틴 오버헤드를 상쇄하고 병렬 처리가 유의미한 이점을 제공하는 지점은 코드 특성과 환경에 따라 다르니, 적절한 값을 찾기 위해 직접 테스트해보는 것이 최선이에요!