Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: sync/atomic: deprecate AddXxx, CompareAndSwapXxx, LoadXxx, StoreXxx, SwapXxx #55302

Closed
uji opened this issue Sep 21, 2022 · 21 comments
Closed

Comments

@uji
Copy link
Contributor

uji commented Sep 21, 2022

Go 1.19 defines new atomic types Bool, Int32, Int64, Uint32, Uint64, Uintptr, and Pointer (release note)

Using these types is more convenient and secure than using AddXxx, CompareAndSwapXxx, LoadXxx, StoreXxx, SwapXxx.
(ex. func AddInt32(addr *int32, delta int32) (new int32))

Moreover, the performance is almost the same, we do not believe there are any situations where AddXxx, CompareAndSwapXxx, LoadXxx, StoreXxx, SwapXxx should be used, and deprecation would limit the user's choices and improve the experience.

Is there a reason that we can not deprecate these functions?

@uji uji added the Proposal label Sep 21, 2022
@gopherbot gopherbot added this to the Proposal milestone Sep 21, 2022
@mvdan
Copy link
Member

mvdan commented Sep 22, 2022

Here's a reason to not deprecate them: if an existing exported struct type has an int64 field used with atomics like StoreInt64, changing that field type to atomic.Int64 would technically be a breaking change. For example:

type Foo {
    Counter int64
}
--
type Foo {
    Counter atomic.Int64 // breaking change
}

We could still add a hint to the old APIs to point out that the new named types exist, though.

@randall77
Copy link
Contributor

I don't think there is any need to deprecate the old functions. They work. There's nothing wrong with them.

I think if someone was starting a new codebase they should use the new types+methods, but in existing codebases there's no reason to force/nudge people to use the new way.
So maybe a comment saying "consider using ... instead" would be ok, but deprecation is more strident than that.

@go101
Copy link

go101 commented Sep 23, 2022

The old way is more memory efficient than new ways on 32-bit architectures, which is another reason it should not be deprecated.

@zikaeroh
Copy link
Contributor

The old way is more memory efficient than new ways on 32-bit architectures, which is another reason it should not be deprecated.

That doesn't seem right; I thought that part of the reason these types were added was so they'd get compiler support to prevent misalignment on those architectures. You might get a smaller struct if you don't use them, but your atomic operations might not work either, right?

@bcmills
Copy link
Contributor

bcmills commented Sep 23, 2022

@mvdan, it's true that changing the type of an existing field would be a breaking change, but “avoiding a breaking API change” is IMO one of the few good reasons to add new usage of a deprecated API.

@bcmills
Copy link
Contributor

bcmills commented Sep 23, 2022

@randall77

They work. There's nothing wrong with them.

I guess that's true of the 32-bit functions, but for the 64-bit functions there is a prominent BUG comment that also gets rendered on https://pkg.go.dev/sync/atomic and perhaps in other places.

It may be true that “they work” subject to that caveat, but that seems like a far cry from “nothing wrong with them”. 😅

The Pointer variant also has the downside of being inherently type-unsafe to use. (The other variants are often also type-unsafe in practice, since one may end up converting between a named integer type and the operand type, but they're at least theoretically usable for variables of unadorned types.)

@mvdan
Copy link
Member

mvdan commented Sep 23, 2022

@bcmills I guess you're right. By my reasoning that switching to (or from) any named type can be a breaking change, we would never be able to deprecate a type. But that's clearly not right; sometimes there are good reasons to deprecate a type.

@bcmills
Copy link
Contributor

bcmills commented Sep 23, 2022

I think if someone was starting a new codebase they should use the new types+methods, but in existing codebases there's no reason to force/nudge people to use the new way.

Doesn't “deprecate” imply exactly that — avoid in new code, still works for existing code? (That's certainly how I interpret it, but maybe I've spent too long in Google's codebases. 🤔)

@mvdan
Copy link
Member

mvdan commented Sep 23, 2022

Doesn't “deprecate” imply exactly that — avoid in new code, still works for existing code?

I think there's a slight disconnect because some people treat deprecation notices as more imperative. For example, staticcheck uses them as warnings, and some people like me treat staticcheck warnings as CI errors. I would have to go out of my way to tell staticcheck that, in a particular case, that deprecation warning is OK to keep because I must follow semver. Which I guess is fine for the edge case - but that is why "still works for existing code" isn't true out of the box for any staticcheck users.

@bcmills
Copy link
Contributor

bcmills commented Sep 23, 2022

@go101

The old way is more memory efficient than new ways on 32-bit architectures, which is another reason it should not be deprecated.

Which cases are more memory-efficient? The new way does force correct alignment for 64-bit fields, but the implementation of the old functions also requires the user to manually perform that same alignment in portable code.

@randall77
Copy link
Contributor

I would be ok with deprecating just the 64-bit ops on 32-bit platforms.

@go101
Copy link

go101 commented Sep 23, 2022

@bcmills

On 32-bit architectures

  • the size of type A is 12
  • the size of type B is 16.
  • the size of type C is 16
  • the size of type D is 24

If C and D are used as array elements, then the latter will consume more memory.

type A struct {
    x int64
    y int32
}

type B struct {
    x atomic.Int64
    y int32
}

type C struct {
    A
    x int32
}

type D struct {
    B
    x int32
}

@bcmills
Copy link
Contributor

bcmills commented Sep 23, 2022

the size of type D is 24

Is that a bug in the compiler implementation of align64?

The documentation for unsafe.Sizeof says:

For a struct, the size includes any padding introduced by field alignment.

but I don't see anything that would require unsafe.Offsetof(D{}.x) >= unsafe.Sizeof(B{}). 🤔

@go101
Copy link

go101 commented Sep 23, 2022

Maybe the implemenation is to satisfy unsafe.Sizeof(D{}.B) == 16.

@bcmills
Copy link
Contributor

bcmills commented Sep 23, 2022

If I'm reading the current unsafe package docs correctly, unsafe.Sizeof(D{}.B) would be 16 even if unsafe.Offsetof(D{}.x) is only 12.

(That's because unsafe.Sizeof is defined to report the size of a variable declared as var v = D{}.B, not the actual number of bytes occupied by the B field within the struct.)

@go101
Copy link

go101 commented Sep 24, 2022

I'm not sure whether or not it is a bug.
I hope it is a bug so that I don't need to expand nested types to save memory under some scenarios. ;)

package main

import (
	"sync/atomic"
	"unsafe"
)

type T struct {
	_ [0]atomic.Int64
	x int32
}

type A struct {
	T
	v int32
}

type B struct {
	_ [0]atomic.Int64
	x int32
	v int32
}

func main() {
	println(unsafe.Sizeof(A{}), unsafe.Sizeof(B{})) // 16 8
}

@randall77
Copy link
Contributor

@go101 That is not a bug. At least, it is intentional. The Go compiler does not attempt to pack some fields into padding needed by other fields.

It is possible we could do that, but it gets tricky. For instance, this program prints 0:

package main

import "unsafe"

type T struct {
	z [2]*byte
	x int32
	// there is a padding int32 here
}

type A struct {
	t T
	v int32 // can we store v in the padding of t?
}

func f(a *A) {
	a.t = T{}
}
func main() {
	var a A
	// put a value in the padding
	*(*int32)(unsafe.Pointer(4 + uintptr(unsafe.Pointer(&a.t.x)))) = 99
	f(&a)
	// is it still there?
	println(*(*int32)(unsafe.Pointer(4 + uintptr(unsafe.Pointer(&a.t.x)))))
}

The compiler is currently fast-and-loose with data in padding fields - in this case it is easier to zero a T by just writing 24 bytes of zeroes. It would be more complicated to avoid clobbering the padding fields, which would be required if some containing type was using the padding fields for actual data.

@rsc
Copy link
Contributor

rsc commented Sep 28, 2022

Deprecate in Go means that we show a warning for code that uses the existing functions. The old atomics are not broken enough to warrant that kind of warning and the developer time that dealing with the warnings will consume.

(Compare to deprecating MD5, or deprecating math/rand.Read - those are far more likely to be real problems in programs.)

@rsc
Copy link
Contributor

rsc commented Sep 28, 2022

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@rsc
Copy link
Contributor

rsc commented Oct 6, 2022

Based on the discussion above, this proposal seems like a likely decline.
— rsc for the proposal review group

@rsc
Copy link
Contributor

rsc commented Oct 12, 2022

No change in consensus, so declined.
— rsc for the proposal review group

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

8 participants