사칙연산이 가능해야 한다.
예외처리가 확실해야 한다(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 코어를 가지고 있을 때 효과가 더 커집니다.
- 연산이 복잡할수록 고루틴 사용의 효과가 더 두드러집니다.
- 벤치마크 테스트를 통해 실제 성능을 평가하는 것이 중요합니다.
고루틴 오버헤드를 상쇄하고 병렬 처리가 유의미한 이점을 제공하는 지점은 코드 특성과 환경에 따라 다르니, 적절한 값을 찾기 위해 직접 테스트해보는 것이 최선이에요!
'프로그래밍 언어 > Go' 카테고리의 다른 글
[Go] Testing package를 이용한 Test 및 Benchmark (0) | 2024.10.05 |
---|---|
[Go] 네트워크 프로그래밍 (0) | 2024.10.04 |
[Go] 내맘대로 계산기 프로그램 만들기 [1] - ASCII ART (0) | 2024.10.04 |
[Go] 내맘대로 계산기 프로그램 만들기 (2) | 2024.10.03 |
[Go] Visual Studio Code 로 디버깅할 때 (1) | 2024.10.03 |