프로그래밍 언어/Go

[Go] 내 맘대로 계산기 프로그램 만들기 [2] - 사칙연산 및 소요시간 테스트

:) :) 2024. 10. 4. 19:36

사칙연산이 가능해야 한다.

예외처리가 확실해야 한다(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 코어를 가지고 있을 때 효과가 더 커집니다.
  • 연산이 복잡할수록 고루틴 사용의 효과가 더 두드러집니다.
  • 벤치마크 테스트를 통해 실제 성능을 평가하는 것이 중요합니다.

고루틴 오버헤드를 상쇄하고 병렬 처리가 유의미한 이점을 제공하는 지점은 코드 특성과 환경에 따라 다르니, 적절한 값을 찾기 위해 직접 테스트해보는 것이 최선이에요!