2011年12月13日火曜日

.net framework : 指定バイトでアライメントされたバッファを作る

[StructLayout(LayoutKind.Sequential, Pack = 16)] でいいかと思ったらダメでした。 MSDNライブラリによると「各フィールドが構造体の先頭を基準に整列されます」だそうです。

アライメント分だけ多めにバッファを作って、オフセットを計算して使うしか無さそうですね。 こんな感じかな?

byte[] buffer = new byte[size + byteAlignment];
UInt64 p = (UInt64)buffer;
offset = byteAlignment - (int)(p % (UInt64)byteAlignment);
if (offset == byteAlignment)
    offset = 0;

マネージコードでbyte配列を作っただけだとガベージコレクタの都合でバッファが移動されることがあります。 そうなるとズレて最初のアライメントが無駄になってしまいます。 それを防ぐためにGCHandleでバッファをロックし続ける...というのは止めた方が良さそうな予感がします。 ライフサイクルが長いバッファをロックし続けるとメモリの使用効率が落ちるケースがあるからです。

ガベージコレクションを邪魔しないようにっていう意味でも、アンマネージコード上でバッファを確保するのが良いのかなぁ? ってことでコードは書いてみました。 ただ、このコードでホントに良いかを試すのは大変そうですよね? ってことで簡単な動作確認しかしていません。 使うアテはあるので、それで実際に動かしてみてダメだったら修正/削除します。

// AlignedBuffer.cs
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

namespace AlignedBufferTest
{
    public class AlignedBuffer : IDisposable
    {
        public const int ByteAlignmentMax = 1073741824;
        public const int ByteAlignmentMin = 4;

        private IntPtr _buffer;
        private int _size;
        private int _byteAlignment;
        private int _offset;
        private object _lockObj = new object();

        // byteAlignment → 4, 8, 16, 32, 64, 128, ... , ByteAlignmentMax
        public AlignedBuffer(int size, int byteAlignment, bool clear)
        {
            if (!IsValidByteAlignment(byteAlignment))
                throw new ArgumentException("byteAlignmentの値が不正。");
            if(size <= 0)
                throw new ArgumentException("sizeの値が不正。");

            _buffer = Marshal.AllocHGlobal(size + byteAlignment - 1);
            _size = size;
            _byteAlignment = byteAlignment;

            UInt64 p = (UInt64)_buffer;
            _offset = byteAlignment - (int)(p % (UInt64)byteAlignment);
            if (_offset == byteAlignment)
                _offset = 0;

            if (clear)
                Clear();
        }

        public IntPtr ThreadUnsafePointer
        {
            get
            {
                return _buffer + _offset;
            }
        }

        public IntPtr Lock()
        {
            Monitor.Enter(_lockObj);
            return ThreadUnsafePointer;
        }

        public void Unlock()
        {
            Monitor.Exit(_lockObj);
        }

        public void Dispose()
        {
            Marshal.FreeHGlobal(_buffer);
            _buffer = IntPtr.Zero;
        }

        public void Clear()
        {
            lock (_lockObj)
            {
                ZeroMemory(_buffer, (IntPtr)(_size + _byteAlignment - 1));
            }
        }

        ~AlignedBuffer()
        {
            if (_buffer != IntPtr.Zero)
                Debug.WriteLine("AlignedBuffer : Disposeし忘れ。");

            Dispose();
        }

        private bool IsValidByteAlignment(int byteAlignment)
        {
            for (int i = ByteAlignmentMin; i <= ByteAlignmentMax; i <<= 1)
                if (byteAlignment == i)
                    return true;

            return false;
        }

        [DllImport("Kernel32.dll", EntryPoint = "RtlZeroMemory", SetLastError = false)]
        private static extern void ZeroMemory(IntPtr dest, IntPtr size);
    }
}

こんなコードはCPUの拡張命令を使わない限り必要ないですよね。 アタリが良ければマネージコードで書いてもJITが勝手に拡張命令を使ってくれるらしいんですけど、その詳細はよく分かってません。 下手にunsafeと拡張命令を使うくらいならそっちを期待した方がいいんでしょうか?

一応、簡単なサンプルコードを書いたので載せておきます。 背景に市松模様が書かれたウィンドウが開き、そこに適当なコードで描いた画像とそれをネガポジ反転した画像が表示されます。

  • AlignedBufferTest.exe ... wpfで作った本体。
  • SSE2Test.dll ... vc++で作ったdll。sse2でネガポジ反転をする。

まずはwpfの方から。 MainWindow.xamlはこうなっています。

<Window x:Class="AlignedBufferTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
        <ScrollViewer.Background>
            <DrawingBrush Viewport="0,0,0.25,0.25" TileMode="Tile">
                <DrawingBrush.Drawing>
                    <DrawingGroup>
                        <GeometryDrawing Brush="White">
                            <GeometryDrawing.Geometry>
                                <RectangleGeometry Rect="0,0,100,100" />
                            </GeometryDrawing.Geometry>
                        </GeometryDrawing>
                        <GeometryDrawing Brush="LightGray">
                            <GeometryDrawing.Geometry>
                                <GeometryGroup>
                                    <RectangleGeometry Rect="0,0,50,50" />
                                    <RectangleGeometry Rect="50,50,50,50" />
                                </GeometryGroup>
                            </GeometryDrawing.Geometry>
                        </GeometryDrawing>
                    </DrawingGroup>
                </DrawingBrush.Drawing>
            </DrawingBrush>
        </ScrollViewer.Background>
        <StackPanel Orientation="Horizontal">
            <Image Name="srcImage" Stretch="None"/>
            <Image Name="dstImage" Stretch="None"/>
        </StackPanel>
    </ScrollViewer>
</Window>

MainWindow.xaml.csです。

// MainWindow.xaml.cs
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace AlignedBufferTest
{
    [StructLayout(LayoutKind.Explicit)]
    public struct Pixel
    {
        [FieldOffset(0)]
        public System.UInt32 C;

        [FieldOffset(0)]
        public byte B;
        [FieldOffset(1)]
        public byte G;
        [FieldOffset(2)]
        public byte R;
        [FieldOffset(3)]
        public byte A;
    }

    public partial class MainWindow : Window
    {
        public const int ImageWidth = 480;
        public const int ImageHeight = 480;

        public MainWindow()
        {
            InitializeComponent();

            int size = ImageWidth * ImageHeight * 4;
            AlignedBuffer srcBuffer = new AlignedBuffer(size, 16, true);
            AlignedBuffer dstBuffer = new AlignedBuffer(size, 16, true);

            Paint(srcBuffer);
            Dll.InvertColors(
                dstBuffer.ThreadUnsafePointer,
                srcBuffer.ThreadUnsafePointer,
                size
            );

            srcImage.Source = CreateBitmap(srcBuffer);
            dstImage.Source = CreateBitmap(dstBuffer);
        }

        private void Paint(AlignedBuffer buffer)
        {
            unsafe
            {
                Pixel* basePointer = (Pixel*)buffer.ThreadUnsafePointer;
                for (int y = 0; y < ImageHeight; y++)
                {
                    for (int x = 0; x < ImageWidth; x++)
                    {
                        Pixel* p = &basePointer[ImageWidth * y + x];

                        p->A = CalcIntensity(x, y, 240, 240, 300);
                        if (p->A != 0)
                        {
                            p->B = CalcIntensity(x, y, 400, 200, 200);
                            p->G = CalcIntensity(x, y, 150, 300, 150);
                            p->R = CalcIntensity(x, y, 100, 400, 400);
                        }
                    }
                }
            }
        }

        private byte CalcIntensity(int x, int y, int cx, int cy, double radius)
        {
            int dx = cx - x;
            int dy = cy - y;
            double v = Math.Sqrt(dx * dx + dy * dy);

            if (v < radius)
                return (byte)(v / radius * 128.0 + 128.0);
            else
                return 0;
        }

        private WriteableBitmap CreateBitmap(AlignedBuffer buffer)
        {
            WriteableBitmap res = new WriteableBitmap(
                ImageWidth,
                ImageHeight,
                96,
                96,
                PixelFormats.Bgra32,
                null
            );

            int stride = ImageWidth * 4;
            int size = stride * ImageHeight;
            res.WritePixels(
                new Int32Rect(0, 0, ImageWidth, ImageHeight),
                buffer.ThreadUnsafePointer,
                size,
                stride
            );

            return res;
        }
    }
}

Dll.csでSSE2Test.dllをインポートします。

// Dll.cs
using System;
using System.Runtime.InteropServices;

namespace AlignedBufferTest
{
    class Dll
    {
        [DllImport("SSE2Test.dll")]
        public extern static void InvertColors(IntPtr dst, IntPtr src, int size);
    }
}

以下、vc++プロジェクトのSSE2Test.dllのコードです。 自動生成されたコードが多いけど省略。 sse2を使用するため、stdafx.hの最後に次の1行を追加します。

#include <emmintrin.h>

本体部分のヘッダファイルは、

// SSE2Test.h
extern "C"
{
    __declspec(dllexport) void InvertColors(char* dst, char* src, const int size);
}

本体部分のコードは、

// SSE2Test.cpp
#include "stdafx.h"

extern "C"
{
    __declspec(dllexport) void InvertColors(char* dst, char* src, const int size)
    {
        __m128i srcValue;
        __m128i dstValue;
        __m128i white = _mm_set1_epi32(0xffffffff);
        __m128i colorMask = _mm_set1_epi32(0x00808080);
        __m128i alphaMask = _mm_set1_epi32(0x80000000);

        for(int i = 0; i < size; i += 16)
        {
            srcValue = _mm_load_si128( (__m128i*)src);
            dstValue = _mm_subs_epi8(white, srcValue);
            _mm_maskmoveu_si128(srcValue, alphaMask, dst);
            _mm_maskmoveu_si128(dstValue, colorMask, dst);

            src += 16;
            dst += 16;
        }
    }
}

InvertColorsに渡すsrcアドレスは16バイトの境界に揃えなければならないのですが、面倒なのでサンプルコードではチェックしていません。 まぁサンプルって事でひとつ。