Skip to content

runtime: receiving from a closed and empty channel still requires acquiring a lock every time #77522

Description

@delichik

While investigating the performance of cgocallback, I noticed that runtime.chanrecv1 accounts for 34.52% of CPU time in runtime.cgocallbackg1, and there is a runtime.lock that accounts for 25.63% in it.

flamegraph

I traced this to a strange lock operation in runtime/cgocall.go:432:

	if gp.m.ncgo == 0 {
		// The C call to Go came from a thread not currently running
		// any Go. In the case of -buildmode=c-archive or c-shared,
		// this call may be coming in before package initialization
		// is complete. Wait until it is.
		<-main_init_done
	}

The issue is that main_init_done is always empty and will be closed when all init() functions return. However, receiving from a closed and empty channel still requires acquiring a lock every time, which seems unnecessary.

Fix

I fixed this issue by removing the unnecessary locking when receiving on a closed and empty channel.

Benchmark

To demonstrate the impact of this change, I created a cgo-callback testcase (cgocallback_benchmark):

  • AddInt: call Go function AddInt from C, 10000000 times per thread

  • ComplexCalculation: call Go function ComplexCalculation from C, 100000 times per thread. ComplexCalculation has a loop to calculate the sum of squares from 1 to 5000

Test Environment: Tests were performed on a Hygon C86 3250 8-core Processor. (Note: I don't have access to other suitable CPUs at the moment.)

Go Version: I'm using master-9777ceceec8fee294d038182739cab7c845ad2d1 to do the test, and also have same problem in 1.25/1.26.

Testing results with master:

Test                              AvgOpsPerSec      TotalDurationNs        TotalOps
AddInt_1threads                     9793121.04           1021124926        10000000
ComplexCalc_1threads                 516999.06            193423949          100000
AddInt_4threads                    11109484.78           3600527007        40000000
ComplexCalc_4threads                2027384.27            197298561          400000
AddInt_8threads                     6597038.09          12126654247        80000000
ComplexCalc_8threads                3829358.90            208912254          800000

Testing results with this fix:

Test                              AvgOpsPerSec      TotalDurationNs        TotalOps
AddInt_1threads                    10451387.02            956810802        10000000
ComplexCalc_1threads                 520229.92            192222699          100000
AddInt_4threads                    41411557.45            965913925        40000000
ComplexCalc_4threads                2077128.25            192573569          400000
AddInt_8threads                    83177629.66            961797064        80000000
ComplexCalc_8threads                4142030.24            193141999          800000

As you can see, ComplexCalc is slightly faster than before. More importantly, AddInt almost eliminates all the overhead of thread-increasing in this testcase, demonstrating that this fix significantly improves the performance of cgocallback scenarios.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ImplementationIssues describing a semantics-preserving change to the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Performancecompiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions