优化整数系数列表和其长整数表示之间的转换

8
我正在尝试优化我的多项式实现。特别地,我处理的是具有模数n(可能为>2^64)和模式为x^r - 1(其中r<2^64)的多项式。目前,我将系数表示为整数列表(*),并以最直观的方式实现了所有基本操作。
我希望幂运算和乘法能够尽可能快地进行,并且为了实现这一点,我已经尝试了不同的方法。我的当前方法是将系数列表转换为大整数,将整数相乘,然后解压回系数。
问题是打包和解包需要很长时间。
那么,有没有办法改进我的“打包/解包”功能呢?
def _coefs_to_long(coefs, window):
    '''Given a sequence of coefficients *coefs* and the *window* size return a
    long-integer representation of these coefficients.
    '''

    res = 0
    adder = 0
    for k in coefs:
        res += k << adder
        adder += window
    return res
    #for k in reversed(coefs): res = (res << window) + k is slower


def _long_to_coefs(long_repr, window, n):
    '''Given a long-integer representing coefficients of size *window*, return
    the list of coefficients modulo *n*.
    '''

    mask = 2**window - 1
    coefs = [0] * (long_repr.bit_length() // window + 1)
    for i in xrange(len(coefs)):
        coefs[i] = (long_repr & mask) % n
        long_repr >>= window

    # assure that the returned list is never empty, and hasn't got an extra 0.
    if not coefs:
        coefs.append(0)
    elif not coefs[-1] and len(coefs) > 1:
        coefs.pop()

    return coefs

请注意,我并未选择n,这是用户的输入。我的程序想要证明n是质数(使用AKS测试),因此我无法对其进行分解。
(*) 我尝试了几种方法:
1. 使用numpy数组而不是列表,并使用numpy.convolve进行乘法。 对于n < 2 ^ 64很快,但对于n > 2 ^ 64非常慢[我也想避免使用外部库]
2. 使用scipy.fftconvolve。 对于n > 2 ^ 64根本不起作用。
3. 从一开始就将系数表示为整数(而不是每次都将它们转换)。 问题是我不知道如何轻松地执行mod x ^ r -1操作,而不将整数转换为系数列表(这违反了使用此表示的原因)。

@harold 我还没有尝试过。最近时间不太充裕。我搜索了那篇文章,只发现有售的版本。也许你知道是否有一些免费发布的在线版本?最终我会购买它。也许在此之前我会自己研究BDDs。 - Bakuriu
@Bakuriu,这是链接:http://cecs.uci.edu/~papers/compendium94-03/papers/1995/edt95/pdffiles/09a_3.pdf 这是一篇简短的论文,没有详细解释0抑制BDD的细节,但你可以在其他地方找到相关信息。 - harold
哇,谢谢。我会在有时间的时候研究一下它(不幸的是这意味着下周,因为这个周末我很忙)。 - Bakuriu
@J.F.Sebastian 事实是我已经阅读并尝试了那些方法,而且我发现我的方法更快。 - Bakuriu
显示剩余12条评论
4个回答

2

除非你是为了学习而这样做,否则为什么要重复造轮子呢?另一种方法是编写一个Python包装器来调用某个其他的多项式库或程序,如果这样的包装器不存在的话。

试试PARI/GP。它的速度惊人地快。我最近写了一段自定义的C代码,花了我两天时间来写,结果只比两行PARI/GP脚本快3倍。我敢打赌,调用PARI的Python代码将比你单独在Python中实现的任何代码都更快。甚至还有一个模块可以从Python中调用PARI: https://code.google.com/p/pari-python/


我正在寻找这样的优化,因为我“也”想学习。 我已经编写了一个C扩展,使用flint库进行计算,根据任务不同,速度提高了10到80倍(我已经知道一些优化此C实现的方法,特别是因为我知道可以使用带有“_”前缀的函数来避免大量重新分配,因为我知道可以使用多项式的最大大小)。 感谢您指出PARI/GP,我之前不知道,我会研究一下。 无论如何,我希望能够得到一个纯Python的高效多项式实现。 - Bakuriu
1
好的!也许我写的应该是一条评论而不是一个答案——我还在适应 Stack Overflow。 - Douglas B. Staple

2
您可以尝试使用剩余数系统来表示多项式的系数。您也可以像现在一样将系数拆分为较小的整数,但不需要将它们转换回一个巨大的整数来进行乘法或其他操作。这应该不需要太多的重新编程工作。
剩余数系统的基本原理是使用模算术对数字进行唯一表示。围绕RNS的整个理论使您能够在小系数上执行操作。
编辑: 一个快速的例子:
假设您使用模数11和13的RNS来表示您的大系数。您的系数将全部由2个小整数(<11和<13)组成,可以组合成原始(大)整数。
假设您的多项式最初是33x²+18x+44。在RNS中,系数分别为(33 mod 11, 33 mod 13),(18 mod 11,18 mod 13)和(44 mod 11, 44 mod 13)=>(0,7),(7,5)和(0,5)。
将多项式乘以一个常数可以通过将每个小系数乘以该常数并对其进行模运算来完成。比如,如果你乘以3,你的系数将变为(0,21 mod 13)=(0,8),(21 mod 11,15 mod 13)=(10,2)和(0 mod 11,15 mod 13)=(0,2)。没有必要将系数转换回它们的大整数形式。
为了检查我们的乘法是否有效,我们可以将新系数转换回它们的大表示形式。这需要将每组系数作为模系统进行“求解”。对于第一组系数(0,8),我们需要解决x mod 11=0和x mod 13=8。这不应该太难实现。在这个例子中,你可以看到x=99是一个有效的解(模13*11)。
然后我们得到99x²+54x+132,正确的乘积多项式。与其他多项式相乘类似(但需要对系数进行成对相乘)。加法也是如此。
对于你的用例,你可以根据你想要的系数数量或它们的大小选择n。

你能给我指一些关于RNS的更详细的文章/书籍吗? 不管怎样,你是说我应该用一定数量的较小整数来表示系数,然后使用这些较小数字的数组进行操作(这可能会让我使用numpy)? - Bakuriu
我没有具体的参考资料,但是有很多教程可以参考,比如这个这个这个。任何一个都应该能帮助你入门。 我认为numpy是可行的。 - Origin
该算法的基础是中国剩余定理,它保证了在RNS中数字的唯一表示。如果您想了解更多信息,可以查看证明,这也非常有趣。 - Origin
你能解释一下如何高效地执行模n运算吗?在我看来,我必须将每个系数转换为十进制,取模,然后重新转换为RNS。但这种转换并不高效。 - Bakuriu
我添加了一个例子。你应该在 (..,..) 系数上执行所有操作,并且只有在需要时将它们转换回它们的大尺寸。 - Origin
抱歉,我怀疑这不适合我的解决方案。 “n” 是由用户作为输入提供的,我的程序想要证明其是否是质数,因此我不能仅仅将其分解以获得小模数来获取RNS表示形式。我必须选择一组模数,以便我可以表示所有小于“n”的数字,然后所有操作必须对“n”取模,而我认为使用这种表示方法不是很有效率。 - Bakuriu

2
直接将任意精度整数多项式实现为numpy数组列表如何? 让我解释一下:假设您的多项式是Σp Ap Xp。 如果可以表示大整数Ap = Σk Ap,k 2 ^ 64 k,则第k个numpy数组将在位置p处包含64位int Ap,k。 根据问题的结构,可以选择密集或稀疏数组。 实现加法和标量操作只是将相同操作的bignum实现向量化的问题。 可以按以下方式处理乘法:AB = Σp,k,p',k' Ap,kBp',k' 2 ^ 64(k + k')Xp + p'。 因此,具有密集数组的天真实现可能会导致对numpy.convole或scipy.fftconvolve的log64(n)^ 2次调用。模运算应该很容易实现,因为它是左侧项的线性函数,而右侧项的系数很小。 而不是将多项式表示为任意精度数字列表(它们本身表示为64位“数字”的列表),请转置表示,以便:多项式表示为数组列表,第k个数组包含每个系数的第k个“数字”。 如果只有少数系数非常大,则数组中将主要包含0,因此使用稀疏数组可能值得。 将Ap,k称为第p个系数的第k个数字。 注意与大整数表示的类比:其中大整数将表示为x = Σk xk 2 ^ 64 k,您的多项式A以与之相同的方式表示为A = Σk Ak 2 ^ 64 k Ak = Σk Ap,k Xp。 要实现加法,您只需假装数组列表是简单数字的列表,并像通常一样为大整数实现加法(注意要用numpy.where替换if then条件语句)。

为了实现乘法,你将需要进行 log64(n)2 次多项式乘法。

要在系数上实现模运算,只需将大整数的模运算转化即可。

要对小系数的多项式取模,使用此操作的线性特性:

A mod (Xr - 1) = (Σk Ak 264 k) mod (Xr - 1)

= Σk 264 k (Ak mod (Xr - 1))


你能再解释一下这个想法吗?另外,你写的 2^64.k,点是乘法吗? 无论如何,正如我所说,我想不使用第三方库来完成这个任务,尽管你的解决方案很有趣。 - Bakuriu
是的,点号代表乘法,但我已经将其删除了。我添加了一些细节,不确定哪个部分需要更多的澄清。。 - spam_eggs
好的,我现在能清楚地看到整个情况了,但是,唉,直到下周二我都非常忙,没有时间来实现和研究你的解决方案。 - Bakuriu
如果你选择这条路,你将面临巨大的工作压力,但如果你的目标是学习,那么你会学到很多!然而,由于我发现这个问题非常有趣,所以我写了一个部分实现,也许可以帮助你开始。我将在这里发布其中的一部分… - spam_eggs
我一直在尝试实现这个功能(很抱歉这么晚了!),但是有一些我不理解的地方。据我所知,每当我执行 Rp,k = Ap,k + Bp,k 并且结果大于 2^64 时,我应该将进位加到 Rp,k+1 上,但是numpy不会对这种情况发出警告,因此我将堆栈检查结果中的每个系数,并尝试手动判断是否存在溢出,最终手动添加进位。有没有使用numpy的更聪明的方法来解决这个问题? - Bakuriu
是的,一般的想法是你需要处理溢出。但是通过将半字大小的数字存储在一个完整的字中,可以使这个过程变得更简单。我在这里实现了一个例子:https://github.com/gsidier/bigpoly - 查看bigpoly.HalfInt和HalfPoly类。由于溢出,多项式乘法仍然存在错误,所以请耐心等待... Poly64类展示了另一种(更复杂的方式)通过显式编码进位逻辑来完成它。 - spam_eggs

1

我找到了一种优化转换的方法,尽管我仍然希望有人能够帮助我进一步改进它们,并希望找到其他聪明的想法。

基本上,这些函数存在某种二次内存分配行为,当打包整数或解包整数时会出现问题。 (请参见Guido van Rossum的this帖子中的另一个此类行为的示例)。

在意识到这一点后,我决定尝试使用“分而治之”的原则,并获得了一些结果。我只是将数组分成两部分,分别进行转换,最后将结果合并(稍后我将尝试使用类似于Rossum帖子中的f5的迭代版本[编辑:似乎没有更快])。

修改后的函数:

def _coefs_to_long(coefs, window):
    """Given a sequence of coefficients *coefs* and the *window* size return a
    long-integer representation of these coefficients.
    """

    length = len(coefs)
    if length < 100:
        res = 0
        adder = 0
        for k in coefs:
            res += k << adder
            adder += window
        return res
    else:
        half_index = length // 2
        big_window = window * half_index
        low = _coefs_to_long(coefs[:half_index], window)
        high = _coefs_to_long(coefs[half_index:], window)
        return low + (high << big_window)


def _long_to_coefs(long_repr, window, n):
    """Given a long-integer representing coefficients of size *window*, return
    the list of coefficients modulo *n*.
    """

    win_length = long_repr.bit_length() // window
    if win_length < 256:
        mask = 2**window - 1
        coefs = [0] * (long_repr.bit_length() // window + 1)
        for i in xrange(len(coefs)):
            coefs[i] = (long_repr & mask) % n
            long_repr >>= window

        # assure that the returned list is never empty, and hasn't got an extra 0.
        if not coefs:
            coefs.append(0)
        elif not coefs[-1] and len(coefs) > 1:
            coefs.pop()

        return coefs
    else:
        half_len = win_length // 2
        low = long_repr & (((2**window) ** half_len) - 1)
        high = long_repr >> (window * half_len)
        return _long_to_coefs(low, window, n) + _long_to_coefs(high, window, n) 

结果如下:

>>> import timeit
>>> def coefs_to_long2(coefs, window):
...     if len(coefs) < 100:
...         return coefs_to_long(coefs, window)
...     else:
...         half_index = len(coefs) // 2
...         big_window = window * half_index
...         least = coefs_to_long2(coefs[:half_index], window) 
...         up = coefs_to_long2(coefs[half_index:], window)
...         return least + (up << big_window)
... 
>>> coefs = [1, 2, 3, 1024, 256] * 567
>>> # original function
>>> timeit.timeit('coefs_to_long(coefs, 11)', 'from __main__ import coefs_to_long, coefs',
...               number=1000)/1000
0.003283214092254639
>>> timeit.timeit('coefs_to_long2(coefs, 11)', 'from __main__ import coefs_to_long2, coefs',
...               number=1000)/1000
0.0007998988628387451
>>> 0.003283214092254639 / _
4.104536516782767
>>> coefs = [2**64, 2**31, 10, 107] * 567
>>> timeit.timeit('coefs_to_long(coefs, 66)', 'from __main__ import coefs_to_long, coefs',...               number=1000)/1000

0.009775240898132325
>>> 
>>> timeit.timeit('coefs_to_long2(coefs, 66)', 'from __main__ import coefs_to_long2, coefs',
...               number=1000)/1000
0.0012255229949951173
>>> 
>>> 0.009775240898132325 / _
7.97638309362875

正如您所看到的,此版本将转换速度提高了相当多,从4倍加快到8倍(输入越大,加速越快)。 第二个函数也可以得到类似的结果:

>>> import timeit
>>> def long_to_coefs2(long_repr, window, n):
...     win_length = long_repr.bit_length() // window
...     if win_length < 256:
...         return long_to_coefs(long_repr, window, n)
...     else:
...         half_len = win_length // 2
...         least = long_repr & (((2**window) ** half_len) - 1)
...         up = long_repr >> (window * half_len)
...         return long_to_coefs2(least, window, n) + long_to_coefs2(up, window, n)
... 
>>> long_repr = coefs_to_long([1,2,3,1024,512, 0, 3] * 456, 13)
>>> # original function
>>> timeit.timeit('long_to_coefs(long_repr, 13, 1025)', 'from __main__ import long_to_coefs, long_repr', number=1000)/1000
0.005114212036132813
>>> timeit.timeit('long_to_coefs2(long_repr, 13, 1025)', 'from __main__ import long_to_coefs2, long_repr', number=1000)/1000
0.001701267957687378
>>> 0.005114212036132813 / _
3.006117885794327
>>> long_repr = coefs_to_long([1,2**33,3**17,1024,512, 0, 3] * 456, 40)
>>> timeit.timeit('long_to_coefs(long_repr, 13, 1025)', 'from __main__ import long_to_coefs, long_repr', number=1000)/1000
0.04037192392349243
>>> timeit.timeit('long_to_coefs2(long_repr, 13, 1025)', 'from __main__ import long_to_coefs2, long_repr', number=1000)/1000
0.005722791910171509
>>> 0.04037192392349243 / _
7.0545853417694

我尝试避免在第一个函数中进行更多的内存重新分配,传递开始和结束索引并避免切片,但结果是对于小输入,这会使函数变慢很多,而对于实际情况的输入则稍微慢一些。也许我可以尝试混合它们,尽管我不认为我会获得更好的结果。


我最近修改了我的问题,因此有些人给了我一些与我最近所需不同的建议。我认为有必要澄清评论和答案中不同来源指出的结果,以便它们对其他寻求实现快速多项式和/或AKS测试的人有用。

正如J.F. Sebastian所指出的,AKS算法得到了许多改进,因此尝试实现旧版本的算法将始终导致非常缓慢的程序。这并不排除如果您已经有一个良好的AKS实现,可以通过改进多项式来加速它。
如果您对小n(即:字长数字)的系数感兴趣,并且不介意外部依赖,则可以使用numpy并使用numpy.convolve或scipy.fftconvolve进行乘法。它比您编写的任何内容都要快得多。不幸的是,如果n不是字长,则根本无法使用scipy.fftconvolve,而且numpy.convolve也会变得非常缓慢。
如果您不必对系数和多项式进行模运算,则可能使用ZBDDs是个好主意(正如harold所指出的),尽管我不能保证会有惊人的结果(尽管我认为这真的很有趣,您应该阅读Minato的论文)。
如果您不必对系数进行模运算,则可能使用RNS表示,如Origin所述,是个好主意。然后,您可以组合多个numpy数组以有效地操作。
如果您想要一个纯Python实现的具有大n模数系数的多项式,则我的解决方案似乎是最快的。尽管我没有尝试在Python中实现系数数组之间的fft乘法(可能会更快)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接