zeromicro / go-zero

A cloud-native Go microservices framework with cli tool for productivity.
https://go-zero.dev
MIT License
29.38k stars 3.97k forks source link

Generate structure fields for memory alignment,can use fieldalignment? #2798

Closed jan-bar closed 1 year ago

jan-bar commented 1 year ago

I have the following table creation statement

CREATE TABLE IF NOT EXISTS `object`
(
    `id`          int(10) unsigned NOT NULL AUTO_INCREMENT,
    `sex`         tinyint(1)       NOT NULL DEFAULT '1' COMMENT '1:man,2:woman',
    `name`        varchar(16)      NOT NULL DEFAULT '',
    `update_time` datetime         NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

Generate related code commands: goctl model mysql ddl --style goZero -d . -s object.sql

Generate the following structure,Sequential source sql table creation statement

type (
    Object struct {
        Id         int64     `db:"id"`
        Sex        int64     `db:"sex"` // 1:man,2:woman
        Name       string    `db:"name"`
        UpdateTime time.Time `db:"update_time"`
    }
)

Install the official inspection tool: go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest

executive command: fieldalignment -fix .,The result is as follows

fieldalignment -fix .
objectModel_gen.go:38:9: struct with 56 pointer bytes could be 32

The final structure is changed to the following order

type (
    Object struct {
        UpdateTime time.Time `db:"update_time"`
        Name       string    `db:"name"`
        Id         int64     `db:"id"`
        Sex        int64     `db:"sex"`
    }
)

Memory alignment will improve performance and reduce GC consumption. I think this feature can support

We can integrate the fix code of fieldalignment into go-zero

kevwan commented 1 year ago

Thanks! PR is apprecated.

jan-bar commented 1 year ago

Thanks! PR is apprecated.

This piece involves logic such as ast source code, I am not very familiar with it, and I will study it after the new year. Happy New Year.

jan-bar commented 1 year ago
  1. Add the following code in the following file tools/goctl/model/sql/converter/types_test.go

    
    func TestGenSortGo(t *testing.T) {
    val := make(map[string]struct{})
    for _, v := range unsignedTypeMap {
        val[v] = struct{}{}
    }
    for _, v := range commonMysqlDataTypeMapInt {
        val[v] = struct{}{}
    }
    for _, v := range commonMysqlDataTypeMapString {
        val[v] = struct{}{}
    }
    val["sql.NullInt64"] = struct{}{}
    val["sql.NullInt32"] = struct{}{}
    val["sql.NullFloat64"] = struct{}{}
    val["sql.NullBool"] = struct{}{}
    val["sql.NullString"] = struct{}{}
    val["sql.NullTime"] = struct{}{}
    
    fw, err := os.Create("sort.go")
    if err != nil {
        panic(err)
    }
    defer fw.Close()
    
    fw.WriteString(`package converter

import ( "database/sql" "time"

"github.com/lib/pq"

)

//go:generate fieldalignment -fix sort.go type FieldSort struct { `) i := 0 for k := range val { fmt.Fprintf(fw, "\tF%02d %s\n", i, k) i++ } fw.WriteString("}") }

2. executive command: `cd tools/goctl/model/sql/converter && go test -v -run TestGenSortGo`, Generate `sort.go`
3. Modify the `optimalOrder` method of the above file as follows `golang.org/x/tools/go/analysis/passes/fieldalignment/fieldalignment.go`
```go
func optimalOrder(str *types.Struct, sizes *gcSizes) (*types.Struct, []int) {
    nf := str.NumFields()

    type elem struct {
        name    string // add name field
        index   int
        alignof int64
        sizeof  int64
        ptrdata int64
    }

    elems := make([]elem, nf)
    for i := 0; i < nf; i++ {
        field := str.Field(i)
        ft := field.Type()

        ss := ft.String()
        if si := strings.LastIndexByte(ss, '/'); si > 0 {
            ss = ss[si+1:]
        }

        elems[i] = elem{
            ss,
            i,
            sizes.Alignof(ft),
            sizes.Sizeof(ft),
            sizes.ptrdata(ft),
        }
    }

    sort.Slice(elems, func(i, j int) bool {
        ei := &elems[i]
        ej := &elems[j]

        // Place zero sized objects before non-zero sized objects.
        zeroi := ei.sizeof == 0
        zeroj := ej.sizeof == 0
        if zeroi != zeroj {
            return zeroi
        }

        // Next, place more tightly aligned objects before less tightly aligned objects.
        if ei.alignof != ej.alignof {
            return ei.alignof > ej.alignof
        }

        // Place pointerful objects before pointer-free objects.
        noptrsi := ei.ptrdata == 0
        noptrsj := ej.ptrdata == 0
        if noptrsi != noptrsj {
            return noptrsj
        }

        if !noptrsi {
            // If both have pointers...

            // ... then place objects with less trailing
            // non-pointer bytes earlier. That is, place
            // the field with the most trailing
            // non-pointer bytes at the end of the
            // pointerful section.
            traili := ei.sizeof - ei.ptrdata
            trailj := ej.sizeof - ej.ptrdata
            if traili != trailj {
                return traili < trailj
            }
        }

        // Lastly, order by size.
        if ei.sizeof != ej.sizeof {
            return ei.sizeof > ej.sizeof
        }

        return false
    })

    // Save all database generation type information to file
    fw, err := os.Create("typesSizeofMap.go")
    if err != nil {
        panic(err)
    }
    defer fw.Close()

    fw.WriteString(`var typesSizeofMap = map[string]struct {
        alignof int64
        sizeof  int64
        ptrdata int64
    }{
`)

    for _, e := range elems {
        fmt.Fprintf(fw, "\t\"%s\": {%d,%d,%d},\n",
            e.name, e.alignof, e.sizeof, e.ptrdata)
    }
    fw.WriteString("}")

    fields := make([]*types.Var, nf)
    indexes := make([]int, nf)
    for i, e := range elems {
        fields[i] = str.Field(e.index)
        indexes[i] = e.index
    }
    return types.NewStruct(fields, nil), indexes
}
  1. cd golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment && go install Compile the modified code
  2. cd tools/goctl/model/sql/converter && go mod tidy && fieldalignment -fix sort.go Generate information data for each field through the sort.go file
  3. Copy the content of file typesSizeofMap.go to tools/goctl/model/sql/gen/field.go
  4. The final tools/goctl/model/sql/gen/field.go file looks like this
    
    package gen

import ( "sort" "strings"

"github.com/zeromicro/go-zero/tools/goctl/model/sql/parser"
"github.com/zeromicro/go-zero/tools/goctl/model/sql/template"
"github.com/zeromicro/go-zero/tools/goctl/util"
"github.com/zeromicro/go-zero/tools/goctl/util/pathx"

)

var typesSizeofMap = map[string]struct { alignof int64 sizeof int64 ptrdata int64 }{ "time.Time": {8, 24, 24}, "sql.NullTime": {8, 32, 24}, "string": {8, 16, 8}, "[]byte": {8, 24, 8}, "pq.Int64Array": {8, 24, 8}, "sql.NullString": {8, 24, 8}, "pq.Float64Array": {8, 24, 8}, "pq.StringArray": {8, 24, 8}, "pq.BoolArray": {8, 24, 8}, "sql.NullFloat64": {8, 16, 0}, "sql.NullInt64": {8, 16, 0}, "float64": {8, 8, 0}, "uint64": {8, 8, 0}, "uint": {8, 8, 0}, "int64": {8, 8, 0}, "sql.NullInt32": {4, 8, 0}, "uint32": {4, 4, 0}, "uint16": {2, 2, 0}, "sql.NullBool": {1, 2, 0}, "uint8": {1, 1, 0}, "byte": {1, 1, 0}, "bool": {1, 1, 0}, }

func genFields(table Table, fields []*parser.Field) (string, error) { var list []string

// Refer to go/analysis/passes/fieldalignment/fieldalignment.go#optimalOrder to add the following sorting logic
sort.Slice(fields, func(i, j int) bool {
    ei, ok := typesSizeofMap[fields[i].DataType]
    if !ok {
        return false
    }

    ej, ok := typesSizeofMap[fields[j].DataType]
    if !ok {
        return false
    }

    zeroi := ei.sizeof == 0
    zeroj := ej.sizeof == 0
    if zeroi != zeroj {
        return zeroi
    }

    if ei.alignof != ej.alignof {
        return ei.alignof > ej.alignof
    }

    noptrsi := ei.ptrdata == 0
    noptrsj := ej.ptrdata == 0
    if noptrsi != noptrsj {
        return noptrsj
    }

    if !noptrsi {
        traili := ei.sizeof - ei.ptrdata
        trailj := ej.sizeof - ej.ptrdata
        if traili != trailj {
            return traili < trailj
        }
    }

    if ei.sizeof != ej.sizeof {
        return ei.sizeof > ej.sizeof
    }

    return false
})

for _, field := range fields {
    result, err := genField(table, field)
    if err != nil {
        return "", err
    }

    list = append(list, result)
}

return strings.Join(list, "\n"), nil

}

func genField(table Table, field *parser.Field) (string, error) { tag, err := genTag(table, field.NameOriginal) if err != nil { return "", err }

text, err := pathx.LoadTemplate(category, fieldTemplateFile, template.Field)
if err != nil {
    return "", err
}

output, err := util.With("types").
    Parse(text).
    Execute(map[string]interface{}{
        "name":       util.SafeString(field.Name.ToCamel()),
        "type":       field.DataType,
        "tag":        tag,
        "hasComment": field.Comment != "",
        "comment":    field.Comment,
        "data":       table,
    })
if err != nil {
    return "", err
}

return output.String(), nil

}

In the end, only modifying the `tools/goctl/model/sql/gen/field.go` file to compile `goctl` can use this feature.

I researched for a half of the day and came up with the above solution. I feel that I am not able to raise PR. I am currently only using this feature in my project by modifying the goctl source code compilation. Hope everyone can add it to the project. @kevwan 

The following two data depend on the `os` and `arch` and `compiler`.  I think different platforms will come up with different `typesSizeofMap`
```go
    wordSize := pass.TypesSizes.Sizeof(unsafePointerTyp)
    maxAlign := pass.TypesSizes.Alignof(unsafePointerTyp)

Maybe the following way is more appropriate

    var (
        typesSizes       = types.SizesFor(build.Default.Compiler, build.Default.GOARCH)
        unsafePointerTyp = types.Unsafe.Scope().Lookup("Pointer").(*types.TypeName).Type()
        wordSize         = typesSizes.Sizeof(unsafePointerTyp)
        maxAlign         = typesSizes.Alignof(unsafePointerTyp)
    )