sonumb

Go - release/debug 모드 빌드하기 - 2 본문

개발자 이야기/Go

Go - release/debug 모드 빌드하기 - 2

sonumb 2021. 11. 4. 16:13

개요

https://sonumb.tistory.com/124

 

Go - release/debug 모드 빌드하기 - 1

개요 빌드 시, 릴리즈 모드에 따라 호출되는 함수의 기능을 달리하고 싶을 때가 있다. "모드에 따른 다른 빌드"를 구현하는데, 두 가지 방법이 있다. 방안들 1. 빌드 시, 태그( -tags ) 옵션을 이용하

sonumb.tistory.com

 

개요 및 전반적인 이야기는 윗 글을 참조하고, 전편에 이어 방안 2에 대한 소스코드를 보여주려한다.

 

'모드에 따른 빌드하기'의 방안 2에 대해 간단히 설명하자면,

  • 빌드시, 빌드모드를 소스코드에 기록하기 (실제로는 하드디스크의 파일의 내용을 바꾼다는 의미가 아니라, 파일 내용을 메모리로 로딩 후, 메모리 내에 변수를 수정한다는 것이다.)
  • 코드에 기록된 빌드모드로 동작

이다.

 

여기서 가장 중요한것은 빌드 모드를 소스코드에 기록하는 것은 "go build -ldflag" 의 '-X' 옵션이다. 이에 대한 설명은 아래 링크에서.

https://pkg.go.dev/cmd/link

 

link command - cmd/link - pkg.go.dev

Link, typically invoked as “go tool link”, reads the Go archive or object for a package main, along with its dependencies, and combines them into an executable binary. Command Line ¶Usage: go tool link [flags] main.a Flags: -B note Add an ELF_NT_GNU_B

pkg.go.dev

소스파일들

main.go 파일

package main

import (
    "fmt"
    "time"
)

func foo() {
    _, _, fnname, err := GetTraceInfo()
    if err == nil {
        println(fnname)
    }

    time.Sleep(time.Second)
    fmt.Println("foo() : done")
}

func bar() {
    Trace()

    time.Sleep(1 * time.Second)
    fmt.Println("bar() : done")
}

func main() {
    foo()
    bar()
}

trace.go 파일

package main

import (
    "fmt"
    "runtime"
    "errors"
    "strings"
)

type TraceFunc func()
const (
    traceMode_Release = iota + 0
    traceMode_Debug
    traceMode_Max
)

var (
    BuildType = "Release"
    traceMode = traceMode_Release
)

func init() {
    switch BuildType {
    case "Release":
        traceMode = traceMode_Release
    case "Debug":
        traceMode = traceMode_Debug
    }

    if strings.EqualFold(BuildType, "debug") == true {
        traceMode = traceMode_Debug
    }
}

func GetTraceInfo() (string, int, string, error) {
    pc, file, line, ok := runtime.Caller(1)
    if ok == false {
        return "", 0, "", errors.New("invalid calling")
    }

    fn := runtime.FuncForPC(pc)
    if fn == nil {
        return "", 0, "", errors.New("unknown method")
    }

    return file, line, fn.Name(), nil
}

var trFunc = [traceMode_Max]TraceFunc {
    trace_Release,
    trace_Debug,
}

func trace_Release() {
    // do nothing
}

func trace_Debug() {
    pc, file, line, ok := runtime.Caller(1)
    if ok == false {
        return
    }
    fnName := ""
    fn := runtime.FuncForPC(pc)
    if fn != nil {
        fnName = fn.Name()
    }

    fmt.Printf("%s:%d %s()\n", file, line, fnName)
}

func Trace()  {
    trFunc[traceMode]()
}

Makefile 파일

all: release

DEBUG := "Debug"
release:
    go build -o buildSystem2

debug:
    go build -ldflags '-X "main.BuildType=Debug"' -o buildSystem2

clean:
    rm -f buildSystem2

실행 및 결과

$ make clean all; ./buildSystem2 
rm -f buildSystem2
go build -o buildSystem2
main.foo
foo() : done
bar() : done

$ make clean debug; ./buildSystem2 
rm -f buildSystem2
go build -ldflags '-X "main.BuildType=Debug"' -o buildSystem2
main.foo
foo() : done
/Users/sonumb/work/go_exam/buildsystem2/trace.go:72 main.Trace()
bar() : done

$

성능 차이

두 방식 간의 성능 차이는 확실히 존재한다. 개념적으로도 함수를 한번 더 호출하는 두 번째 방식이 성능이 떨어질 것으로 예측할 수 있다.

 

릴리즈 모드, 즉 비어있는 Trace() 함수를 10억번 호출하는데, 아래와 같은 차이가 있다.

방안1:
비어 있는 trace()를 호출
방안2:
Trace()함수 내에서 함수포인터를 이용한 호출(if문은 없음)
$ time buildSystem

real 0m0.221s
user 0m0.217s
sys 0m0.003s
$ time buildSystem2

real 0m1.077s
user 0m1.072s
sys 0m0.004s

수치상으로는 5배이지만, 10억회 호출할 동안 다른 일처리(파일, 소켓 I/O)로 인해 이 정도 성능차이는 아주 적게 반영될 것이다.

가령 "10분 0.2초 VS. 10분 1초" 같은 느낌...

 

따라서, 두 방식간의 차이는 성능보단 구현 용이성 및 형상 관리 측면에서 접근하는 것이 옳을 것으로 판단된다.

반응형