簡體   English   中英

如何快速准確地將 64 位 integer 乘以 C# 中的 64 位小數?

[英]How can I quickly and accurately multiply a 64-bit integer by a 64-bit fraction in C#?

SO 上有很多類似的問題,但我還沒有找到一個可以工作並且可以輕松移植到 C# 的問題。大多數涉及 C++ 或類似的問題,並且(大概)工作答案依賴於嵌入式程序集或本機 C/ C# 中不存在的 C++ 函數。幾個函數適用於部分范圍,但在其他部分失敗。 我找到了一個可以移植到 C# 的有效答案,但速度非常慢(事實證明,當我編譯到 x64 而不是 x86 時,它的速度相當快,所以我將其發布為要擊敗的答案)。

問題

我需要一個 function,它允許我將任何 64 位 integer 乘以從兩個 64 位整數派生的 0 到 1(或 -1 到 1)的分數。 理想情況下,答案對 Int64 和 UInt64 都適用,但從另一個開始工作可能並不難。

在我的例子中,我有一個隨機的 64 位 Int64/UInt64(使用 xoshiro256p 算法,盡管這可能無關緊要)。 我想將該數字縮放到類型允許值中的任意范圍。 例如,我可能想將 Int64 縮放到范圍 [1000, 35000]。 從概念上講,這很容易:

 UInt64 minVal = 1000; UInt64 maxVal = 35000; UInt64 maxInt = UInt64.MaxValue; UInt64 randInt = NextUInt64(); // Random value between 0 and maxInt. UInt64 diff = maxVal - minVal + 1; UInt64 scaledInt = randInt * diff / maxInt; // This line can overflow. return scaledInt + minVal;

正如許多其他人和上面的評論所指出的那樣,問題是randInt * diff可能會溢出。

在紙面上,我可以簡單地將中間結果存儲在 128 位 integer 中,然后將除法結果存儲在 64 位 output 中。但是 128 位數學不是 64 位系統的本機,我會而是避免使用任意精度的庫,因為我將多次調用這個 function 並且效率會很顯着。

我可以乘以一個雙精度以獲得 53 位精度,這對我目前正在做的事情來說很好,但我寧願想出一個合適的解決方案。

我可以使用其中一種 ASM 解決方案創建一個 C++ 庫並調用該庫,但我想要一些純 C# 的東西。

要求

  • 需要是純 C#。
  • 需要對任何一組輸入起作用,使得randInt * diff / maxInt在 [0, maxInt] 范圍內(並且每個值本身都在同一范圍內)。
  • 不應該需要外部庫。
  • 需要從數學上正確的答案+-1。
  • 需要相當快。 也許我只是在尋求奇跡,但我覺得如果雙打可以達到 5-10 毫秒,我們應該能夠使用獲得另外 11 位精度的專用代碼達到 20 毫秒。
  • 理想情況下,在發布和調試模式下工作得相對較好。 我的代碼有大約 3:1 的比率,所以我認為我們可以在發布時間的 5 倍以下進行調試。

我的測試

我已經測試了以下解決方案的相對性能。 每個測試運行我的隨機數生成器的 100 萬次迭代,使用各種方法進行縮放。 我首先生成隨機數並將它們放入列表中(一個用於簽名,一個用於未簽名)。 然后我遍歷每個列表並將其縮放為第二個列表。

我最初在調試模式下進行了一系列測試。 這基本上無關緊要(我們正在測試相對性能),但 Int128/UInt128 庫在發布模式下表現更好。

括號中的數字是調試時間。 我將它們包括在這里是因為我仍然希望在調試時有不錯的性能。 例如,Int128 庫非常適合發布模式,但不適合調試。 在您准備好最終發布之前,使用具有更好平衡的東西可能會很有用。 因為我正在測試一百萬個樣本,所以以毫秒為單位的時間也是每次操作以納秒為單位的時間(所有百萬個 UInt64 在 33 毫秒內生成,因此每個在 33 納秒內生成)。

我的測試源代碼可以在 GitGub 上找到

  • 86 毫秒 (267):Int64 隨機生成器。
  • 33 毫秒 (80):UInt64 隨機生成器。
  • 4 毫秒 (5):使用雙精度轉換為 Int64,精度降低。
  • 8 毫秒 (10):同樣適用於 UInt64。
  • 76 ms (197):Int64 的C 代碼,轉換為 C#(具體代碼在我下面的回答中)。
  • 72 毫秒 (187):同樣適用於 UInt64。
  • 54 毫秒 (1458):此 UInt128 庫,用於 Int64。
  • 40 毫秒 (1476):同樣適用於 UInt64。
  • 1446 毫秒 (1455):Int64 的double128庫。 需要付費許可證才能用於商業用途。
  • 1374 毫秒 (1397):再次用於 UInt64。

我無法讓這些給出正確的結果。

類似問題

在 64 位中進行組合乘除運算的最准確方法?

  • phuclv、Soonts、Mysticial 和 500 的回答 - 內部服務器錯誤涉及外部庫、程序集或特定於 MSVC 的函數。
  • timos、AnT、Alexey Frunze 和 Michael Burr 的回答實際上沒有回答任何問題。
  • Serge Rogatch 和 Pubby 的回答並不准確。
  • AProgrammer 的回答有效,但速度很慢(我不知道它是如何工作的)——我最終還是使用了它,並在 x64 編譯中獲得了不錯的結果。

當 x*n 溢出時,如何將 x 減去 n/d?

  • Abhay Aravinda 的唯一答案不是真正的代碼,我不確定如何實現最后一部分,並且評論表明它無論如何都會溢出大值。

integer乘以真分數的快速方法,無浮點數或溢出

  • Taron 和 chux 的回答 - Reinstate Monica 是近似值或特定於 MSVC 的。
  • 回答者 R.. GitHub 停止幫助 ICE 只使用 64 位數學,因為該問題是關於乘以 Int32。

(a * b) / c MulDiv 和處理中間乘法溢出

  • Jeff Penfold 的回答對我不起作用(我認為我在從 Java 轉換為 C# 的邏輯運算符中遺漏了一些東西),而且速度非常慢。
  • greybeard 的回答看起來不錯,但我不確定如何將其翻譯成 C#。
  • tohoho 和 Dave 的回答滿天飛。
  • David Eisenstat 的回答需要 BigInt 庫。

如何將 64 位 integer 乘以 C++ 中的分數,同時最小化錯誤?

  • 所有的答案都在不同的情況下溢出。

但是 128 位數學不是 64 位系統的原生算法

雖然這大部分是正確的,但一種不錯的方法來獲得兩個 64 位整數的完整 128 位乘積: Math.BigMul (適用於 .NET 5 及更高版本)

x64 有一個對應的 128 位輸入除法,這樣一對全乘后跟一個寬除法將實現這個“用適當的分數縮放 integer”操作(限制是分數不能大於1,否則會導致溢出)。 但是,C# 無法訪問寬除法,即使可以訪問,它在大多數硬件上的效率也不會很高。

但是您也可以直接使用 BigMul,因為除數實際上應該是 2 64 (不是 2 64 - 1),並且 BigMul 會自動除以 2 64

所以代碼變成:(未測試)

ulong ignore;
ulong scaled = Math.BigMul(randInt, diff, out ignore);
return scaled + minVal;

對於舊版本的 .NET,可以這樣獲取產品的高 64 位:

static ulong High64BitsOfProduct(ulong a, ulong b)
{
    // decompose into 32bit blocks (in ulong to avoid casts later)
    ulong al = (uint)a;
    ulong ah = a >> 32;
    ulong bl = (uint)b;
    ulong bh = b >> 32;
    // low times low and high times high
    ulong l = al * bl;
    ulong h = ah * bh;
    // cross terms
    ulong x1 = al * bh;
    ulong x2 = ah * bl;
    // carry from low half of product into high half
    ulong carry = ((l >> 32) + (uint)x1 + (uint)x2) >> 32;
    // add up all the parts
    return h + (x1 >> 32) + (x2 >> 32) + carry;
}

不幸的是,它不如Math.BigMul,但至少仍然沒有除法。

通過告訴編譯器不喜歡使用 AnyCpu 設置的 32 位代碼,我能夠使用AProgrammer 的 C 代碼將時間減少到大約 250 毫秒。

在發布模式下,PRNG 占用了大約 5 毫秒(我有點懷疑這一點;我認為當我嘗試只運行 PRNG 時它正在被優化),總時間減少到大約 77 毫秒。

我仍然不確定它是如何工作的,但鏈接的答案說代碼有一些冗余操作以支持 base 10。 我想我可以通過優化 base 10 支持來進一步減少時間,如果我知道它是如何工作的足以做到這一點。

Int64(有符號)有點慢(78 vs 77ms 發布,大約慢 20ms 調試),但我的速度基本相同。 如果 min=Int64.MinValue 和 max=Int64.MaxValue,它確實失敗,每次都返回 min,但適用於我可以拋出的所有其他組合。

帶符號的數學對於直接縮放不太有用。 我剛剛做了一些在我的用例中有用的東西。 所以我做了一個似乎適用於一般簽名案例的轉換,但它可能會被優化一下。

無符號縮放算法,轉換為 C#。

/// <summary>
/// Returns an accurate, 64-bit result from value * multiplier / divisor without overflow.
/// From https://stackoverflow.com/a/8757419/5313933
/// </summary>
/// <param name="value">The starting value.</param>
/// <param name="multiplier">The number to multiply by.</param>
/// <param name="divisor">The number to divide by.</param>
/// <returns>The result of value * multiplier / divisor.</returns>
private UInt64 MulDiv64U(UInt64 value, UInt64 multiplier, UInt64 divisor)
{
    UInt64 baseVal = 1UL << 32;
    UInt64 maxdiv = (baseVal - 1) * baseVal + (baseVal - 1);

    // First get the easy thing
    UInt64 res = (value / divisor) * multiplier + (value % divisor) * (multiplier / divisor);
    value %= divisor;
    multiplier %= divisor;
    // Are we done?
    if (value == 0 || multiplier == 0)
        return res;
    // Is it easy to compute what remain to be added?
    if (divisor < baseVal)
        return res + (value * multiplier / divisor);
    // Now 0 < a < c, 0 < b < c, c >= 1ULL
    // Normalize
    UInt64 norm = maxdiv / divisor;
    divisor *= norm;
    value *= norm;
    // split into 2 digits
    UInt64 ah = value / baseVal, al = value % baseVal;
    UInt64 bh = multiplier / baseVal, bl = multiplier % baseVal;
    UInt64 ch = divisor / baseVal, cl = divisor % baseVal;
    // compute the product
    UInt64 p0 = al * bl;
    UInt64 p1 = p0 / baseVal + al * bh;
    p0 %= baseVal;
    UInt64 p2 = p1 / baseVal + ah * bh;
    p1 = (p1 % baseVal) + ah * bl;
    p2 += p1 / baseVal;
    p1 %= baseVal;
    // p2 holds 2 digits, p1 and p0 one

    // first digit is easy, not null only in case of overflow
    UInt64 q2 = p2 / divisor;
    p2 = p2 % divisor;

    // second digit, estimate
    UInt64 q1 = p2 / ch;
    // and now adjust
    UInt64 rhat = p2 % ch;
    // the loop can be unrolled, it will be executed at most twice for
    // even baseVals -- three times for odd one -- due to the normalisation above
    while (q1 >= baseVal || (rhat < baseVal && q1 * cl > rhat * baseVal + p1))
    {
        q1--;
        rhat += ch;
    }
    // subtract 
    p1 = ((p2 % baseVal) * baseVal + p1) - q1 * cl;
    p2 = (p2 / baseVal * baseVal + p1 / baseVal) - q1 * ch;
    p1 = p1 % baseVal + (p2 % baseVal) * baseVal;

    // now p1 hold 2 digits, p0 one and p2 is to be ignored
    UInt64 q0 = p1 / ch;
    rhat = p1 % ch;
    while (q0 >= baseVal || (rhat < baseVal && q0 * cl > rhat * baseVal + p0))
    {
        q0--;
        rhat += ch;
    }
    // we don't need to do the subtraction (needed only to get the remainder,
    // in which case we have to divide it by norm)
    return res + q0 + q1 * baseVal; // + q2 *baseVal*baseVal
}

MulDiv64使用未簽名的版本進行簽名轉換。 它在我的用例中較慢(290 毫秒與 260 毫秒調試,95 毫秒與 81 毫秒發布),但適用於一般情況。 不適用於 Int64.MinValue(引發異常:“否定二進制補碼的最小值無效。”)。

public static Int64 MulDiv64(Int64 value, Int64 multiplier, Int64 divisor)
{
    // Get the signs then convert to positive values.
    bool isPositive = true;
    if (value < 0) isPositive = !isPositive;
    UInt64 val = (UInt64)Math.Abs(value);
    if (multiplier < 0) isPositive = !isPositive;
    UInt64 mult = (UInt64)Math.Abs(multiplier);
    if (divisor < 0) isPositive = !isPositive;
    UInt64 div = (UInt64)Math.Abs(divisor);

    // Scaledown.
    UInt64 scaledVal = MulDiv64U(val, mult, div);

    // Convert to signed Int64.
    Int64 result = (Int64)scaledVal;
    if (!isPositive) result *= -1;

    // Finished.
    return result;
}

GetRangeU function 返回一個介於最小值和最大值之間的無符號 UInt64,包括在內。 縮放直接來自早期的 function。

/// <summary>
/// Returns a random unsigned integer between Min and Max, inclusive.
/// </summary>
/// <param name="min">The minimum value that may be returned.</param>
/// <param name="max">The maximum value that may be returned.</param>
/// <returns>The random value selected by the Fates for your application's immediate needs. Or their fickle whims.</returns>
public UInt64 GetRangeU(UInt64 min, UInt64 max)
{
    // Swap inputs if they're in the wrong order.
    if (min > max)
    {
        UInt64 Temp = min;
        min = max;
        max = Temp;
    }

    // Get a random integer.
    UInt64 randInt = NextUInt64();

    // Fraction randInt/MaxValue needs to be strictly less than 1.
    if (randInt == UInt64.MaxValue) randInt = 0;

    // Get the difference between min and max values.
    UInt64 diff = max - min + 1;

    // Scale randInt from the range 0, maxInt to the range 0, diff.
    randInt = MulDiv64U(diff, randInt, UInt64.MaxValue);

    // Add the minimum value and return the result.
    return randInt;// randInt + min;
}

GetRange function 返回一個介於最小值和最大值之間的有符號 Int64。 不容易轉換為一般縮放,但在這種情況下它比上面的方法更快。

/// <summary>
/// Returns a random signed integer between Min and Max, inclusive.
/// Returns min if min is Int64.MinValue and max is Int64.MaxValue.
/// </summary>
/// <param name="min">The minimum value that may be returned.</param>
/// <param name="max">The maximum value that may be returned.</param>
/// <returns>The random value selected.</returns>
public Int64 GetRange(Int64 min, Int64 max)
{
    // Swap inputs if they're in the wrong order.
    if (min > max)
    {
        Int64 Temp = min;
        min = max;
        max = Temp;
    }

    // Get a random integer.
    UInt64 randInt = NextUInt64();

    // Fraction randInt/MaxValue needs to be strictly less than 1.
    if (randInt == UInt64.MaxValue) randInt = 0;

    // Get the difference between min and max values.
    UInt64 diff = (UInt64)(max - min) + 1;

    // Scale randInt from the range 0, maxInt to the range 0, diff.
    randInt = MulDiv64U(diff, randInt, UInt64.MaxValue);

    // Convert to signed Int64.
    UInt64 randRem = randInt % 2;
    randInt /= 2;
    Int64 result = min + (Int64)randInt + (Int64)randInt + (Int64)randRem;

    // Finished.
    return result;
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM