1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Office VBA RGBへ変換する関数の高速化

Last updated at Posted at 2023-03-05

基本

OffceTanaka 田中大先生に喧嘩を売る気はないのですが、Color番号(Long型)をRGB つまりRED Green Blueに変換するときは
少なくとも0から255までなのでByteまたはInteger型になります。
Office TANAKA - グラフ[テーマの色について]
しかし、INTで切っているということはColorNumberが必ずLongで入ってくるとすれば、バックスラッシュ(円)演算字(整数除算演算子)を使ってもいいはずです。
なお、この記事では勝手にbackslash Operatorを整数除算演算子と訳します。Slashは除算演算子です。

結論

田中大先生は自分が想像がつかないケースを予測しているのだろう

田中大先生のコードに間違いはない。
除算演算子を使っている点で遅い。
Longで出しているのに結果をStingにしている点でも遅い。
おそらく自分が想定し得ないエラーを見て除算演算子を使用されているのだろう。
それに1億回もカラー変換をやることはなく、速度の差も意識する必要はない。
RGBの出力結果を見ても、同じ結果を得た。
田中大先生に間違いはない。

最速:商を\バックスラッシュ(日本語では¥ )で取得

\演算子 | Microsoft Learnを使う。この演算子については後述。
colornumberには整数を代入すること。

Function ColorNumToRGBHigh(colornumber As Long) As Variant
' Hyspeed \(Slash)Devide
' RGBに変換する。
' ただし ColorNumberは必ず+-(256^3)-1の整数または0
' 小数があると銀行丸めの対象となり、結果がわずかに違ってくる可能性がある
' CIntについても同様
' / 除算演算子 \ 整数除算演算子 VBEではバックスラッシュは円マークになる
' 整数除算演算子は整数の場合にのみ使用する除算演算子。除算結果の小数点以下は切捨
’ 整数でない場合は、変換をして除算を行うが変換不可能なら例外がスローされる
  Dim arLL(0 to 2) As Long
  arLL(0) = colornumber Mod &H100
  arLL(1) = (colornumber \ 256) Mod 256 ' Int(x / 256 ) mod 256
  arLL(2) = colornumber \ 256 \ 256 ' Int(x / 256 / 256)
  ColorNumToRGBHigh = arLL
End Function

最速2:負数を考慮

このIF文はあまり遅くならない。
システムが吐き出したカラーの値はマイナスになることがあるが、それが入るならばこちらが良い。
実際にはこちらを使うべきだと思われる。

Function ColorNumToRGBHigh2(colornumber As Long)
 Function ColorNumToRGBHigh2(colornumber As Long) As Variantと表記してもよい
' 負の整数を考慮した関数
' RGBに変換する。
  Dim arLL(0 To 2) As Long
  Dim buf As String * 8
  If Abs(colornumber) > 16777215 Then Exit Function
  If colornumber < 0 Then
    buf = Hex(colornumber)
    colornumber = Val("&h" & Mid(buf, 3, 6))
  End If
  arLL(0) = colornumber Mod 256
  arLL(1) = colornumber \ 256 Mod 256
  arLL(2) = colornumber \ 256 \ 256
  ColorNumToRGBHigh2 = arLL
 End Function

この実験結果から次のことが推測される

VBAの文字列操作は遅くなる

Stringにするのは非常に遅いことがわかる

16進数は文字列になるので遅い

どうしても文字列になるため、必然的に遅い。

Byte型は型宣言文字がないため、Integerから毎回変換して遅い

マイナスの場合もエラーがある。
エラーがなくても型変換していると思われる。このため早くならない。
以上からやはり通常は使わないほうが良い。

LongとIntegerには差がないのでLongを使って良い。

これはすでに知られている結論である。
むしろIntegerに型変換するので遅い。
ただし、167百万回に1秒程度の差であり、ないに等しいと言える。
このため、実際に適した型を選べばよいが、無理にIntegerをLongにする必要はなく、一般的にLong使用すればよい。Integerは特に数を制限する場合だけ使用すれば良い。

ただしこれらの差は処理が著しく多い場合のみで通常は影響しない

比較すれば差が出るが、27秒が167百万回を起点として、それが4倍になっても1秒にはならない。
1回程度では1秒以下のレベルでしか差が生じず、気にする必要はない。
したがって、エラー処理を行い、可読性が高いコードを採用すべきである。

関数の処理時間を計測するコード

関数は次のコードでテストした。

Sub test()
Dim i As Long
Dim Dt As Date
Dt = Now
Dim bb
For i = 0 To CLng(16777215)
 bb = ColorNumToRGBSuperHighB(i)
Next
Debug.Print CDate(Now - Dt)
End Sub

Now-DTで時間を測る。
16777215は16777216-1
16777216が覚えられないときは256の3乗なので
CLng((256& * 256& * 256&) - 1) このときはLongであることを宣言しないとオーバーフローする。
CLng((2 ^ 8 ^ 3) - 1)
ポイントはそのままではDoubleに一旦型変換するので、それを抑制する点。
田中第先生との比較コードは後述する。

最速への道

If Abs(colornumber) > 16777215 Then Exit Functionは影響しない

過大な整数を入れた場合エラーでハねるようにするのは影響しないため採用。

Function ColorNumToRGBHighe(colornumber As Long)
  Dim arLL(0 To 2) As Long
'  ReDim arLL(2)
  If Abs(colornumber) > 16777215 Then Exit Function
  arLL(0) = colornumber Mod 256
  arLL(1) = colornumber \ 256 Mod 256
  arLL(2) = colornumber \ 256 \ 256
  ColorNumToRGBHighe = arLL
End Function

負数回避

Office TANAKA - グラフ[ColorプロパティとRGB関数について]
では負数になる場合がある。この出だしがffになるという説に従うと

            buf = Hex(colornumber)
            arLL(0) = CByte(Val("&H" & Mid(buf, 7, 2)))
            arLL(1) = CByte(Val("&H" & Mid(buf, 5, 2)))
            arLL(2) = CByte(Val("&H" & Mid(buf, 3, 2)))
            ColorNumToRGBSuperHighL = arLL

ただし負数ではない場合は遅くなる

負数ではない場合は0埋め、ff加算が必要なので

            buf = "FF" & Right("000000" & Hex(colornumber), 6)
            arLL(0) = Val("&H" & Mid(buf, 7, 2))
            arLL(1) = Val("&H" & Mid(buf, 5, 2))
            arLL(2) = Val("&H" & Mid(buf, 3, 2))
            ColorNumToRGBSuperHighB = arLL

となる。しかし、以下のように正の場合まで同様の処理をすると2倍以上の時間がかかる。

Function ColorNumToRGB_Mid_BB(colornumber As Long)
' 負の整数を考慮した関数
' RGBに変換する。
' 文字列で処理するので「遅い」
  Dim arLL(0 To 2) As Byte
  Dim buf As String * 8
  If colornumber < 0 Then
            buf = Hex(colornumber)
            arLL(0) = Val("&H" & Mid(buf, 7, 2))
            arLL(1) = Val("&H" & Mid(buf, 5, 2))
            arLL(2) = Val("&H" & Mid(buf, 3, 2))
            ColorNumToRGB_Mid_BB = arLL
  Else
            buf = "FF" & Right("000000" & Hex(colornumber), 6)
            arLL(0) = Val("&H" & Mid(buf, 7, 2))
            arLL(1) = Val("&H" & Mid(buf, 5, 2))
            arLL(2) = Val("&H" & Mid(buf, 3, 2))
            ColorNumToRGB_Mid_BB = arLL
  End If
End Function

そこで、マイナスの場合だけ処理をする

負数回避 その2

負数はFF0000FFのように並んでいるのだから、最初のFFを取ればいい。

Function ColorNumToRGB_Mid_BB(colornumber As Long) As Variant
' RGBに変換する。
' 負の整数を考慮した関数
' +-16^3の整数のみ。それを超えるものはエラーになる
  Dim arLL(0 To 2) As Long
  Dim buf As String * 8
  If colornumber < 0 Then
   buf = Hex(colornumber)
   colornumber = Val("&h" & Mid(buf, 3, 6))
  End If
  arLL(0) = colornumber Mod 256
  arLL(1) = colornumber \ 256 Mod 256
  arLL(2) = colornumber \ 256 \ 256
  ColorNumToRGB_Mid_BB = arLL
 End Function

効果がみられなかったMOD回避

VBAでMod演算子がオーバーフローした場合 | Excel作業をVBAで効率化

Function vMod(var1, var2)
vMod = var1 - Fix(var1 / var2) * var2
End Function

このように関数にした場合、関数にせずプロシージャ内に記述する方法を試験したが遅くなるようだ。
これはLongでオーバーフローする場合だけである。
型は関数に入るときに宣言しているので、採用しなかった。

全く効果がなかった:Intger型

これは1秒程度確実に遅くなった。
計算回数を考えれば無視できるくらい小さいものの、(256^3)-1という1億6千7百万回計算すると、1秒遅いことがわかる。

Function ColorNumToRGBHighInt(colornumber As Long)
  Dim arLL(0 To 2) As Integer
'  ReDim arLL(2)
  If Abs(colornumber) > 16777216 Then Exit Function
  arLL(0) = colornumber Mod 256
  arLL(1) = colornumber \ 256 Mod 256
  arLL(2) = colornumber \ 256 \ 256
  ColorNumToRGBHighInt = arLL
End Function

ほぼ全く効果がなかった Byte型変換

0から255の値が入るということはByte型のはずである。
しかし、26秒が1回あった程度で30秒から40秒程度と+10秒程度かかる。概ね10秒以上の差がある。
おそらくByte型に変換しており、この処理がIntegerより時間がかかるのが原因だと推測される。
本来は内部ではByteで処理しているはずであるが、この変換により遅延するものと思われる。

Function ColorNumToRGBSuperHighBF(colornumber As Long)
' Hyspeed \(Slash)Devide
' ただし ColorNumberは必ず正またはゼロ
' マイナス不考慮
  Dim arLL(0 To 2) As Byte
'If IsNumeric(colornumber) = False Then Exit Function
'If Abs(colornumber) > 16777215 Then Exit Function
         arLL(0) = colornumber Mod 256
         arLL(1) = (colornumber \ 256) Mod 256
         arLL(2) = colornumber \ 256 \ 256
         ColorNumToRGBSuperHighBF = arLL
End Function

また、マイナスの場合エラーになってとまるので例外処理が必要である。

Function ColorNumToRGB_Mid_BB(colornumber As Long)
' 負の整数を考慮した関数
' RGBに変換する。
' Byte型に変換するらしく最速にはならなかった
  Dim arLL(0 To 2) As Byte
  Dim buf As String * 8
  If Abs(colornumber) > 16777215 Then Exit Function
  If colornumber < 0 Then
            buf = Hex(colornumber)
            colornumber = Val("&h" & Mid(buf, 3, 6))
  End If
  arLL(0) = colornumber Mod 256
  arLL(1) = colornumber \ 256 Mod 256
  arLL(2) = colornumber \ 256 \ 256
  ColorNumToRGB_Mid_BB = arLL
 End Function

'If IsNumeric(colornumber) = False Then Exit Functionは遅い

数字であるかどうか判定するのは+20秒ほど時間がかかる。

要素数を取り出す、Splitで代入するのは遅い

配列で帰ってきているので要素数をつければ、単独で値を取り出せる。
しかし要素数ごとに毎回関数で計算しているため、非常に遅くなる。

R = ColorNumToRGBSuperHigh(i)(0)
G = ColorNumToRGBSuperHigh(i)(1)
B = ColorNumToRGBSuperHigh(i)(2)

のようにすると毎回計算している。

Splitも遅いCLng(Split(buf, ",")(0)

前項と重複するが、このように配列となった文字列から切り出すと遅くなる
一度結果を受けてから、RGBに代入したほうが良い。

田中大先生との値の比較

値を比較した結果0から16777215まですべて同じRGBの組み合わせが得られた。

Sub testC()
Dim i As Long
Dim Dt As Date
Dt = Now
Dim buf
Dim bb
For i = 0 To CLng(16777215)
 buf = convRGB(i)  bb = ColorNumToRGBHigh(i)
  If bb(0) = CLng(Split(buf, ",")(0)) Then
    If bb(1) = CLng(Split(buf, ",")(1)) Then
      If bb(2) = CLng(Split(buf, ",")(2)) Then
      Else
      Stop
      End If
    Else
    Stop
    End If
  Else
  Stop
  End If
 Next
Debug.Print CDate(Now - Dt)
End Sub

先生の関数を少し変えている

Function convRGB(Num As Long)
'http://officetanaka.net/excel/vba/graph/26.htm
    Dim R As Long, G As Long, B As Long
    B = Int(Num / 65536)
    G = Int((Num - (B * 65536)) / 256)
    R = Num - (G * 256) - (B * 65536)
    convRGB = R & ", " & G & "," & B
End Function

Subプロシージャが止まれば値が異なるが、そのようなことはなく、すべての場合について結果が同じだった。よって整数除算演算子を採用することに問題はなく、高速に処理できるという結論となった。

参考サイトほか

\演算子 | Microsoft Learn

注釈
除算が実行される前に、数値式は Byte、Integer、または Long 式に丸められます。
通常、結果のデータ型は、結果が整数であるかどうかに関係なく、Byte、Byte バリアント、Integer、Integer variant、Long、または Long バリアントです。
小数部分はすべて切り捨てられます。 ただし、いずれかの式が Null の場合、結果は Null になります。 Empty の式は 0 として処理されます。

機械翻訳はおかしい。
ここは次のようになる。

通常、result のデータ型は、結果が整数であるかどうかに関係なく、Byte、Byte バリアント、Integer、Integer バリアント、または、Long、 Long バリアントです。

太字の点は原文では

Before division is performed, the numeric expressions are rounded to Byte, Integer, or Long expressions.

となっており、roundedは銀行丸めの可能性があるため、必ず整数を代入すること

Byte データ型 | Microsoft Learn

バイト変数バイト型は、単一で、署名符号のない、8 ビット (1 バイト) の、0 ~ 255 の範囲の数として保存されます。
Byteデータ型は、バイナリ データを格納する場合に便利です。

これでは意味をなさないのでVisual Basic .Netの方をみる。
Byte データ型 | Microsoft Learn

  • 負の数値。 Byte は符号なしの型であるため、負の数を表すことはできません。 Byte 型に評価される式で、単項マイナス (-) 演算子を使用すると、Visual Basic で式が最初に Short に変換されます。
  • 形式変換。 Visual Basic でファイルの読み取りまたは書き込みを行うとき、または DLL、メソッド、およびプロパティを呼び出すときに、データ形式を自動的に変換させることができます。 Byte 変数および配列に格納されているバイナリ データは、そのような形式の変換時に保持されます。 バイナリ データには String 変数を使用しないでください。ANSI 形式と Unicode 形式間の変換時に、その内容が破損する可能性があります。
  • 拡大変換。 Byte データ型は、Short、UShort、Integer、UInteger、Long、ULong、Decimal、Single、または Double に拡大変換されます。 これは、System.OverflowException エラーを発生させることなく、これらの型のいずれかに Byte を変換できることを意味します。
  • 型宣言文字。 Byte には、リテラルの型文字も識別子の型文字も含まれません。
  • 型宣言文字 Byte には、リテラル型文字または識別子型文字がありません。(Type Characters. Byte has no literal type character or identifier type character.)

このためInteger型は常に暗黙的にByte型に変換されることがわかる。

Long データ型 | Microsoft Learn
データ型変換関数 (VBA) | Microsoft Learn

CInt Integer -32,768 から 32,767。小数点以下は丸められます。

CIntも銀行丸めになる可能性があり、注意が必要。

戻り値として配列を受け取る【ExcelVBA】 | VBA Create
この動的配列を宣言するという指摘は非常に参考になった。

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?