Skip to content

Increased variance in some benchmark tests #87324

Closed

Description

@AndyAyersMS

We have recently noticed a mysterious increase in variance for a number of benchmark tests. For example

MicroBenchmarks.Serializers.Json_FromStream.SystemTextJson_SourceGen_

newplot - 2023-06-09T084514 695

Zooming in to the recent history we see that the variance changes started around June 3.

newplot - 2023-06-09T084559 497

One hypothesis is that the variance is caused by Dynamic PGO, but Dynamic PGO was enabled on May 19th and performance improved at that point and remained steady for two weeks afterwards. So, while Dynamic PGO may be involved there is seemingly some other causal factor here too.

Looking at this benchmark across os/isa we see for this particular test the variance was only an issue for win-x64-intel (pink)

image

I will collate a few more examples here to see if anything obvious jumps out.

Activity

  1. ghost added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Jun 9, 2023
  2. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Jun 9, 2023
  3. removed
    untriagedNew issue has not been triaged by the area owner
    on Jun 9, 2023
  4. self-assigned this
    on Jun 9, 2023
  5. added this to the 8.0.0 milestone on Jun 9, 2023
  6. added
    Priority:2Work that is important, but not critical for the release
    on Jun 9, 2023
  7. AndyAyersMS commented on Jun 9, 2023

    @AndyAyersMS
    MemberAuthor

    Looking at secondary counters it seems some aspect of GC may have changed -- fewer Gen0 GCs

    newplot - 2023-06-09T085546 735

    If we zoom in like above we see the GC change is well correlated with the perf change (hard to get the x-axis exactly the same here, but it's close).

    newplot - 2023-06-09T090927 098

    image

    A plausible commit range here is 7febca6...2f18320

    but there does not seem to be any obvious culprit in that range.

  8. AndyAyersMS commented on Jun 9, 2023

    @AndyAyersMS
    MemberAuthor

    Many of the other tests in #87179 have similar behaviors, though most of those seem to have stabilized as of late.

  9. AndyAyersMS commented on Jun 9, 2023

    @AndyAyersMS
    MemberAuthor

    System.Collections.ContainsTrue<Int32>.ICollection(Size: 512)'s variance seems to be related to enabling Dynamic PGO
    newplot - 2023-06-09T093952 868

    image

    PGO history
    newplot - 2023-06-09T094407 087

    NonPGO (until May 19) history

    newplot - 2023-06-09T094438 228

    So this is not new behavior.

    @EgorBo possibly a case for testing if reservoir size is too small.


    this one also seems to have fixed itself lately
    newplot (74)

  10. AndyAyersMS commented on Jun 17, 2023

    @AndyAyersMS
    MemberAuthor

    The first test in this issue now seems to have calmed down:

    newplot - 2023-06-16T193915 520

    Possibly because of the larger reservoir table size for class probes in #87332

  11. AndyAyersMS commented on Jun 21, 2023

    @AndyAyersMS
    MemberAuthor
  12. AndyAyersMS commented on Jun 21, 2023

    @AndyAyersMS
    MemberAuthor

    Some analysis I did for #87179 suggests one idea to try. With the scalable profile mode #84427 and pervasive sparse profiling #80481, during count reconstruction we often compute the difference between counter values; and if that value is negative, we set the count to zero. But zero count has a special meaning with PGO (see eg #48778) and it may be the jit is getting overly hung up here by the zero, and this happens randomly from run to run.

    We could validate this by seeing if the PGO counts for the benchmarks above are large enough that we switch from deterministic to randomized counting (currently this happens when the count reaches 8192).

    So one idea is to set the diff count for these inconsistent cases to something small or nonzero, say 1% of the larger/smaller value being subtracted, since large counts have a 2% or so margin of error.

  13. AndyAyersMS commented on Jun 22, 2023

    @AndyAyersMS
    MemberAuthor

    The hottest method in System.Collections.CtorFromCollection(Int32).SortedSet(Size: 512) is PickPivotAndPartition, and the gathered PGO data for this has counts in the millions:

    MethodName: System.Collections.Generic.GenericArraySortHelper`1[System.Int32].PickPivotAndPartition
    Signature: int32  (value class System.Span`1<!0>)
    Schema InstrumentationKind 385 ILOffset 191 Count 1 Other 211
    3445760
    Schema InstrumentationKind 385 ILOffset 221 Count 1 Other 241
    3431936
    Schema InstrumentationKind 385 ILOffset 252 Count 1 Other 261
    3142656
    Schema InstrumentationKind 385 ILOffset 285 Count 1 Other 293
    223456
    Schema InstrumentationKind 385 ILOffset 293 Count 1 Other 0
    231424
    Schema InstrumentationKind 385 ILOffset 241 Count 1 Other 275
    224224
    Schema InstrumentationKind 385 ILOffset 221 Count 1 Other 211
    5056512
    Schema InstrumentationKind 385 ILOffset 211 Count 1 Other 241
    0
    Schema InstrumentationKind 385 ILOffset 191 Count 1 Other 181
    4142592
    

    So it certainly seems likely that we could run into the problem noted above, where we subtract two large approximate counts and end up with a negative result.

  14. AndyAyersMS commented on Aug 1, 2023

    @AndyAyersMS
    MemberAuthor

    Also possibly related (for non-windows): #89340. Looks like the fix (#89350) might have worked on RegexRedux above:
    newplot - 2023-08-01T152302 065

  15. AndyAyersMS commented on Aug 1, 2023

    @AndyAyersMS
    MemberAuthor

    Back to the intial benchmark above
    newplot - 2023-08-01T152823 255

    Non-PGO is more stable (note: different axes)

    newplot - 2023-08-01T154459 578

    Linux-x64 (same HW), win-amd-x64 (different HW), win-intel-x86 (same HW) are all stable:

    image

    When run locally this benchmark doesn't warm up enough to reliably make it to Tier1 code when PGO is enabled

    ;; NO PGO
    
    Samples for corerun: 5885 events for Benchmark Intervals
    Jitting           : 00.61% 2.3E+05  samples 2920 methods
      JitInterface    : 00.29% 1.1E+05  samples
    Jit-generated code: 80.13% 3.04E+07 samples
      Jitted code     : 80.13% 3.04E+07 samples
      MinOpts code    : 00.00% 0        samples
      FullOpts code   : 02.64% 1E+06    samples
      Tier-0 code     : 00.00% 0        samples
      Tier-1 code     : 77.50% 2.94E+07 samples
      R2R code        : 00.00% 0        samples
    
    02.16%   8.2E+05     ?        Unknown
    16.47%   6.25E+06    native   coreclr.dll
    05.16%   1.96E+06    Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.BufferElement()
    04.69%   1.78E+06    Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.Read()
    03.90%   1.48E+06    Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.ComputeQuotedTextLengthUntilEndQuote(unsigned int8[],int32,int32,bool&)
    03.74%   1.42E+06    Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.TryGetByte(unsigned int8&)
    02.85%   1.08E+06    Tier-1   [System.Private.DataContractSerialization]XmlObjectSerializerReadContextComplexJson.GetJsonMemberIndex(class System.Runtime.Serialization.XmlReaderDelegator,class System.Xml.XmlDictionaryString[],int32,class System.Runtime.Serialization.ExtensionDataObject)
    02.06%   7.8E+05     Tier-1   [System.Private.DataContractSerialization]XmlBufferReader.GetChars(int32,int32,wchar[])
    01.98%   7.5E+05     Tier-1   [System.Private.DataContractSerialization]XmlBaseReader.GetAttributeNode(class System.String)
    01.92%   7.3E+05     Tier-1   [System.Private.CoreLib]Buffer.Memmove(unsigned int8&,unsigned int8&,unsigned int)
    01.79%   6.8E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.ParseAndSetLocalName()
    01.71%   6.5E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.SkipWhitespaceInBufferReader()
    01.61%   6.1E+05     Tier-1   [System.Private.CoreLib]Buffer._Memmove(unsigned int8&,unsigned int8&,unsigned int)
    01.53%   5.8E+05     Tier-1   [System.Private.DataContractSerialization]XmlBufferReader.SetWindow(int32,int32)
    01.48%   5.6E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.get_Value()
    01.48%   5.6E+05     Tier-1   [System.Private.DataContractSerialization]XmlObjectSerializerReadContextComplexJson.ReadAttributes(class System.Runtime.Serialization.XmlReaderDelegator)
    01.42%   5.4E+05     Tier-1   [System.Private.DataContractSerialization]XmlBaseReader.AddAttribute(value class QNameType,bool)
    01.37%   5.2E+05     Tier-1   [System.Private.DataContractSerialization]XmlBufferReader.TryEnsureBytes(int32)
    01.37%   5.2E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.ReadQuotedText(bool)
    01.34%   5.1E+05     Tier-1   [System.Private.DataContractSerialization]StringHandle.Equals2(class System.Xml.XmlDictionaryString)
    01.29%   4.9E+05     Tier-1   [System.Private.DataContractSerialization]StringHandle.GetString()
    01.29%   4.9E+05     Tier-1   [System.Private.DataContractSerialization]XmlBaseReader.IsStartElement(class System.Xml.XmlDictionaryString,class System.Xml.XmlDictionaryString)
    01.26%   4.8E+05     Tier-1   [System.Private.CoreLib]Buffer.BlockCopy(class System.Array,int32,class System.Array,int32,int32)
    01.16%   4.4E+05     FullOpt  [System.Private.DataContractSerialization]dynamicClass.ReadMyEventsListerItemTaskFromJson(pMT: 00007FFEF92F8F08,pMT: 00007FFEF92F9F90,pMT: 00007FFEF90FC720,pMT: 00007FFEF90FC720[])
    01.13%   4.3E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.UnescapeJsonString(class System.String)
    01.11%   4.2E+05     Tier-1   [System.Private.CoreLib]PackedSpanHelpers.IndexOf(int16&,int16,int32)
    
    ;; PGO
    
    Samples for corerun: 2960 events for Benchmark Intervals
    Jitting           : 10.13% 1.71E+06 samples 3382 methods
      JitInterface    : 03.73% 6.3E+05  samples
    Jit-generated code: 59.36% 1E+07    samples
      Jitted code     : 59.36% 1E+07    samples
      MinOpts code    : 00.00% 0        samples
      FullOpts code   : 01.30% 2.2E+05  samples
      Tier-0 code     : 00.00% 0        samples
      Tier-1 code     : 38.57% 6.51E+06 samples
      R2R code        : 00.00% 0        samples
      ???     code    : 19.49% 3.29E+06 samples
    
    01.48%   2.5E+05     ?        Unknown
    30.57%   5.16E+06    native   coreclr.dll
    06.40%   1.08E+06    native   clrjit.dll
    05.21%   8.8E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.Read()
    03.08%   5.2E+05     Tier-1   [System.Private.DataContractSerialization]XmlObjectSerializerReadContextComplexJson.ReadAttributes(class System.Runtime.Serialization.XmlReaderDelegator)
    02.73%   4.6E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.ParseAndSetLocalName()
    02.19%   3.7E+05     Tier-1   [System.Private.DataContractSerialization]XmlObjectSerializerReadContextComplexJson.GetJsonMemberIndex(class System.Runtime.Serialization.XmlReaderDelegator,class System.Xml.XmlDictionaryString[],int32,class System.Runtime.Serialization.ExtensionDataObject)
    02.13%   3.6E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.BufferElement()
    01.54%   2.6E+05     Tier-1   [System.Private.CoreLib]Buffer._Memmove(unsigned int8&,unsigned int8&,unsigned int)
    01.48%   2.5E+05     Tier-0 instr  [System.Private.DataContractSerialization]XmlJsonReader.BufferElement()
    01.24%   2.1E+05     Tier-0 instr  [System.Private.DataContractSerialization]XmlJsonReader.ComputeQuotedTextLengthUntilEndQuote(unsigned int8[],int32,int32,bool&)
    01.18%   2E+05       native   ntdll.dll
    01.13%   1.9E+05     Tier-1   [System.Private.DataContractSerialization]XmlBufferReader.GetChars(int32,int32,wchar[])
    01.07%   1.8E+05     Tier-1   [System.Private.CoreLib]Buffer.Memmove(unsigned int8&,unsigned int8&,unsigned int)
    01.07%   1.8E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.SkipWhitespaceInBufferReader()
    01.01%   1.7E+05     Tier-0 instr  [System.Private.DataContractSerialization]XmlBufferReader.GetChars(int32,int32,wchar[])
    01.01%   1.7E+05     Tier-1   [System.Private.DataContractSerialization]JsonEncodingStreamWrapper.Read(value class System.Span`1<unsigned int8>)
    01.01%   1.7E+05     Tier-1   [System.Private.DataContractSerialization]XmlJsonReader.ReadQuotedText(bool)
    
  16. AndyAyersMS commented on Aug 2, 2023

    @AndyAyersMS
    MemberAuthor

    So possibly the variance is from using median results in the lab where the iteration to iteration times drop sharply once everything is finally tiered up (note on my machine PGO is quite typically always faster)

    ;; NO PGO
    
    Benchmark: found 15 intervals; mean interval 251.623ms
    000 2294.063 -- 2542.249 : 248.186
    001 2546.449 -- 2799.059 : 252.610
    002 2802.730 -- 3050.554 : 247.825
    003 3056.880 -- 3305.412 : 248.532
    004 3308.075 -- 3566.228 : 258.153
    005 3568.341 -- 3819.385 : 251.044
    006 3821.477 -- 4072.770 : 251.293
    007 4075.155 -- 4323.337 : 248.182
    008 4325.514 -- 4580.522 : 255.008
    009 4582.815 -- 4842.185 : 259.370
    010 4844.435 -- 5095.872 : 251.437
    011 5098.113 -- 5345.018 : 246.906
    012 5347.131 -- 5598.981 : 251.850
    013 5601.411 -- 5857.899 : 256.487
    014 5859.995 -- 6107.452 : 247.456
    
    ;; PGO
    Benchmark: found 20 intervals; mean interval 75.331ms
    000 1636.264 -- 1827.258 : 190.994
    001 1831.155 -- 1990.969 : 159.814
    002 1994.720 -- 2150.555 : 155.835
    003 2156.857 -- 2309.821 : 152.964
    004 2312.489 -- 2456.557 : 144.068
    005 2458.735 -- 2519.465 : 60.730
    006 2521.642 -- 2566.947 : 45.305
    007 2569.012 -- 2614.709 : 45.696
    008 2616.834 -- 2661.702 : 44.868
    009 2664.251 -- 2709.118 : 44.867
    010 2711.482 -- 2764.246 : 52.764
    011 2766.325 -- 2812.219 : 45.894
    012 2814.447 -- 2858.831 : 44.384
    013 2860.918 -- 2905.947 : 45.028
    014 2908.110 -- 2952.605 : 44.494
    015 2954.662 -- 2999.483 : 44.822
    016 3001.610 -- 3046.505 : 44.896
    017 3049.055 -- 3093.696 : 44.641
    018 3095.846 -- 3140.506 : 44.660
    019 3142.694 -- 3192.583 : 49.888
    

    Here's the entire set of these tests, locally. "diff" is with PGO, "base" is without.

    enchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2)
    Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
    .NET SDK 8.0.100-preview.4.23260.5
    [Host] : .NET 6.0.20 (6.0.2023.32017), X64 RyuJIT AVX2
    Job-KLSCSY : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
    Job-DEMYRR : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2

    PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true IterationTime=250.0000 ms
    MaxIterationCount=20 MinIterationCount=15 WarmupCount=1

    Method Job Toolchain Mean Error StdDev Median Min Max Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
    DataContractJsonSerializer Job-KLSCSY \base-rel\corerun.exe 2,299.0 us 44.09 us 43.30 us 2,288.1 us 2,226.6 us 2,392.7 us 1.00 0.00 81.8182 18.1818 538.51 KB 1.00
    DataContractJsonSerializer Job-DEMYRR \diff-rel\corerun.exe 2,984.8 us 1,618.93 us 1,864.36 us 1,868.4 us 1,749.1 us 6,699.7 us 1.42 0.84 86.9565 - 538.52 KB 1.00
    SystemTextJson_Reflection Job-KLSCSY \base-rel\corerun.exe 263.6 us 3.32 us 3.10 us 262.7 us 259.0 us 269.8 us 1.00 0.00 12.5000 2.0833 77.74 KB 1.00
    SystemTextJson_Reflection Job-DEMYRR \diff-rel\corerun.exe 223.0 us 3.67 us 3.07 us 224.0 us 218.2 us 228.3 us 0.85 0.01 12.1845 1.7406 77.74 KB 1.00
    SystemTextJson_SourceGen Job-KLSCSY \base-rel\corerun.exe 263.4 us 3.60 us 3.19 us 262.9 us 259.3 us 270.4 us 1.00 0.00 11.6525 2.1186 77.74 KB 1.00
    SystemTextJson_SourceGen Job-DEMYRR \diff-rel\corerun.exe 213.6 us 2.82 us 2.64 us 212.6 us 210.3 us 218.9 us 0.81 0.01 12.0172 1.7167 77.74 KB 1.00
    JSON.NET Job-KLSCSY \base-rel\corerun.exe 527.6 us 10.16 us 9.50 us 525.5 us 508.1 us 543.1 us 1.00 0.00 25.3700 4.2283 157.38 KB 1.00
    JSON.NET Job-DEMYRR \diff-rel\corerun.exe 410.6 us 4.05 us 3.79 us 409.9 us 406.3 us 418.1 us 0.78 0.01 24.6305 4.9261 157.38 KB 1.00
    Jil Job-KLSCSY \base-rel\corerun.exe 380.2 us 5.04 us 4.21 us 379.4 us 374.6 us 391.1 us 1.00 0.00 16.9753 3.0864 106.11 KB 1.00
    Jil Job-DEMYRR \diff-rel\corerun.exe 332.3 us 6.60 us 6.18 us 333.3 us 322.3 us 341.7 us 0.87 0.02 16.5816 2.5510 106.11 KB 1.00
    Utf8Json Job-KLSCSY \base-rel\corerun.exe 279.6 us 5.18 us 4.84 us 279.4 us 273.7 us 288.4 us 1.00 0.00 16.7411 2.2321 105.72 KB 1.00
    Utf8Json Job-DEMYRR \diff-rel\corerun.exe 208.7 us 3.79 us 3.54 us 209.1 us 202.7 us 215.3 us 0.75 0.02 16.6667 2.5000 105.72 KB 1.00

    Adding

    [MinWarmupCount(6, forceAutoWarmup: true)]
    [MaxWarmupCount(10, forceAutoWarmup: true)]

    Gives better numbers with PGO enabled (diff)

    BenchmarkDotNet v0.13.7-nightly.20230717.35, Windows 11 (10.0.22621.1992/22H2/2022Update/SunValley2)
    Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
    .NET SDK 8.0.100-preview.4.23260.5
    [Host] : .NET 6.0.20 (6.0.2023.32017), X64 RyuJIT AVX2
    Job-VTIKZT : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2
    Job-MABZQL : .NET 8.0.0 (42.42.42.42424), X64 RyuJIT AVX2

    PowerPlanMode=00000000-0000-0000-0000-000000000000 Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true IterationTime=250.0000 ms
    MaxIterationCount=20 MaxWarmupIterationCount=10 MinIterationCount=15
    MinWarmupIterationCount=6 WarmupCount=-1

    Method Job Toolchain Mean Error StdDev Median Min Max Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
    DataContractJsonSerializer Job-VTIKZT \base-rel\corerun.exe 2,359.7 us 46.45 us 51.63 us 2,347.3 us 2,293.8 us 2,499.0 us 1.00 0.00 87.3786 19.4175 538.51 KB 1.00
    DataContractJsonSerializer Job-MABZQL \diff-rel\corerun.exe 1,861.2 us 46.03 us 51.17 us 1,852.0 us 1,795.7 us 1,980.6 us 0.79 0.03 83.3333 - 538.52 KB 1.00
    SystemTextJson_Reflection Job-VTIKZT \base-rel\corerun.exe 268.7 us 3.41 us 3.02 us 268.1 us 263.2 us 274.4 us 1.00 0.00 11.7773 2.1413 77.74 KB 1.00
    SystemTextJson_Reflection Job-MABZQL \diff-rel\corerun.exe 229.6 us 3.18 us 2.82 us 229.9 us 224.3 us 235.4 us 0.85 0.01 10.8696 - 77.74 KB 1.00
    SystemTextJson_SourceGen Job-VTIKZT \base-rel\corerun.exe 265.6 us 2.80 us 2.48 us 265.8 us 261.8 us 269.8 us 1.00 0.00 11.6525 2.1186 77.74 KB 1.00
    SystemTextJson_SourceGen Job-MABZQL \diff-rel\corerun.exe 215.6 us 4.30 us 3.81 us 214.0 us 212.1 us 225.5 us 0.81 0.02 11.9760 1.7109 77.74 KB 1.00
    JSON.NET Job-VTIKZT \base-rel\corerun.exe 540.9 us 7.67 us 6.80 us 543.2 us 531.1 us 551.9 us 1.00 0.00 24.2826 4.4150 157.38 KB 1.00
    JSON.NET Job-MABZQL \diff-rel\corerun.exe 438.9 us 16.12 us 16.55 us 434.7 us 417.8 us 477.2 us 0.81 0.04 24.3902 - 157.38 KB 1.00
    Jil Job-VTIKZT \base-rel\corerun.exe 393.4 us 7.50 us 7.70 us 389.8 us 383.4 us 404.5 us 1.00 0.00 16.9231 3.0769 106.11 KB 1.00
    Jil Job-MABZQL \diff-rel\corerun.exe 326.8 us 4.97 us 4.65 us 327.1 us 319.8 us 333.8 us 0.83 0.02 16.9713 2.6110 106.11 KB 1.00
    Utf8Json Job-VTIKZT \base-rel\corerun.exe 285.4 us 5.01 us 4.44 us 284.7 us 279.0 us 296.0 us 1.00 0.00 16.2037 2.3148 105.72 KB 1.00
    Utf8Json Job-MABZQL \diff-rel\corerun.exe 221.8 us 3.46 us 3.24 us 222.2 us 216.7 us 227.1 us 0.78 0.02 16.7254 2.6408 105.72 KB 1.00
  17. AndyAyersMS commented on Aug 4, 2023

    @AndyAyersMS
    MemberAuthor

    It looks like there are a variety of explanations:

    I am going to close this as I don't see anything actionable right now; we can revisit paritcular cases if they prove problematic.

  18. ghost locked as resolved and limited conversation to collaborators on Sep 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Priority:2Work that is important, but not critical for the releasearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions