Golang中的大小数据加载(将uint16放入uint8切片中)

Golang中的大小数据加载(将uint16放入uint8切片中)

问题描述:

I'm hacking together a rough ISS of a processor and I wonder if there is a more efficient way to to what I am doing (ideally without resorting to the unsafe library). (Simplified) I'm representing memory by:

type DataMem []uint8

a register by:

type Register uint16

Memory needs to be in byte sized units and the processor works in larger units as above. This means to store the data we do:

func (m *Machine) ExecuteST (ins Instruction) {
  // Store to memory
  // Data to store specified in one register
  // Address to store to specified by another register
  target_address := m.RegBank[ins.Dst]
  src_data := m.RegBank[ins.Src]

  ///////// Start of annoying section////////
  var target_0 uint8
  var target_1 uint8
  target_0 =  src_data & 0x0f
  target_1 = (src_data & 0xf0)>>8
  m.data_mem[target_address  ] = target_0
  m.data_mem[target_address+1] = target_1
  /////////// End of annoying section /////////
  m.logger.Printf("ST To Address %x, Data %x
",target_address,src_data)
}

And so on for all sorts of different data types and transfers.

The annoying thing for me is I know the processor that the go code will be running on will have a single load instruction that could do all of the above in a single transfer. I'd expect a good c compiler to optimise into that for me, but without breaking out the unsafe library and going straight to the pointers I don't think I can get around this?

I'm open for suggestions including better ways to model the data memory...

Because of the type safety of Go, there's not much you can do differently here without using the unsafe package.

The simplest solution, and probably the most performant, is to do the conversion on the target address:

// convert the target address to *uint16
srcData = uint16(0xeeee)
*(*uint16)(unsafe.Pointer(&dataMem[targetAddress])) = srcData

Similarly, another option is to shadow the []uint8 slice with a []uint16 slice pointing to the same memory region. This looks even more hairy, and you have to check your offsets and alignment yourself.

dataMem16 := (*(*[1<<30 - 1]uint16)(unsafe.Pointer(&dataMem[0])))[:len(dataMem)/2 : len(dataMem)/2]
dataMem16[targetAddress/2] = srcData

One other option to try is using the builtin copy to move the bytes. Since copy is implemented in assembly by the compiler, it may do what you want (though I haven't checked what the assembly of the conversion actually produces, so it could be a wash)

copy(dataMem[targetAddress:], (*(*[2]uint8)(unsafe.Pointer(&srcData)))[:])

Or as an inline-able function as @OneOfOne shows:

func regToMem(reg uint16) *[2]uint8 {
    return (*[2]uint8)(unsafe.Pointer(&reg))
}

copy(mem[targetAddress:], regToMem(0xffff)[:])

https://play.golang.org/p/0c1UywVuzj

If performance is critical, and you want to use architecture specific instructions, the "proper" way to do this would be to implement what you want directly in assembly, but the code generated from the first solution would probably be hard to beat.

Since what I care about is efficiency of the simulation another option is to declare the data memory differently e.g.

type DataMem []uint32

The provide methods that given an address exctract the byte

func (dm *DataMem) StoreB (address int) uint8 {...}
func (dm *DataMem) Store16 (address int) uint16 {...}

Given this is running on a PC there will be a data cache there so fetching too large a data word will not cost anything on a modern processor.

I need a new cardinal rule, when you have a tricky problem, look at the data structures and your platform ;-)